From d7107e4c67c75cb7f8d967fbaa1fdfd8da7eafd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Tue, 22 Sep 2020 13:41:23 +0100 Subject: [PATCH 01/92] [Upgrade Assistant] Rename "telemetry" to "stats" (#78127) --- .../public/application/components/tabs.tsx | 2 +- .../tabs/checkup/deprecations/reindex/button.tsx | 2 +- .../server/routes/telemetry.test.ts | 16 ++++++++-------- .../upgrade_assistant/server/routes/telemetry.ts | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx index 146cebabbb382..110eff36e3df9 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx @@ -239,7 +239,7 @@ export class UpgradeAssistantTabs extends React.Component { this.setState({ telemetryState: TelemetryState.Running }); - await this.props.http.fetch('/api/upgrade_assistant/telemetry/ui_open', { + await this.props.http.fetch('/api/upgrade_assistant/stats/ui_open', { method: 'PUT', body: JSON.stringify(set({}, tabName, true)), }); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx index a20f4117f693d..747430f455f22 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx @@ -239,7 +239,7 @@ export class ReindexButton extends React.Component { }); afterEach(() => jest.clearAllMocks()); - describe('PUT /api/upgrade_assistant/telemetry/ui_open', () => { + describe('PUT /api/upgrade_assistant/stats/ui_open', () => { it('returns correct payload with single option', async () => { const returnPayload = { overview: true, @@ -51,7 +51,7 @@ describe('Upgrade Assistant Telemetry API', () => { const resp = await routeDependencies.router.getHandler({ method: 'put', - pathPattern: '/api/upgrade_assistant/telemetry/ui_open', + pathPattern: '/api/upgrade_assistant/stats/ui_open', })( routeHandlerContextMock, createRequestMock({ body: returnPayload }), @@ -72,7 +72,7 @@ describe('Upgrade Assistant Telemetry API', () => { const resp = await routeDependencies.router.getHandler({ method: 'put', - pathPattern: '/api/upgrade_assistant/telemetry/ui_open', + pathPattern: '/api/upgrade_assistant/stats/ui_open', })( routeHandlerContextMock, createRequestMock({ @@ -93,7 +93,7 @@ describe('Upgrade Assistant Telemetry API', () => { const resp = await routeDependencies.router.getHandler({ method: 'put', - pathPattern: '/api/upgrade_assistant/telemetry/ui_open', + pathPattern: '/api/upgrade_assistant/stats/ui_open', })( routeHandlerContextMock, createRequestMock({ @@ -108,7 +108,7 @@ describe('Upgrade Assistant Telemetry API', () => { }); }); - describe('PUT /api/upgrade_assistant/telemetry/ui_reindex', () => { + describe('PUT /api/upgrade_assistant/stats/ui_reindex', () => { it('returns correct payload with single option', async () => { const returnPayload = { close: false, @@ -121,7 +121,7 @@ describe('Upgrade Assistant Telemetry API', () => { const resp = await routeDependencies.router.getHandler({ method: 'put', - pathPattern: '/api/upgrade_assistant/telemetry/ui_reindex', + pathPattern: '/api/upgrade_assistant/stats/ui_reindex', })( routeHandlerContextMock, createRequestMock({ @@ -147,7 +147,7 @@ describe('Upgrade Assistant Telemetry API', () => { const resp = await routeDependencies.router.getHandler({ method: 'put', - pathPattern: '/api/upgrade_assistant/telemetry/ui_reindex', + pathPattern: '/api/upgrade_assistant/stats/ui_reindex', })( routeHandlerContextMock, createRequestMock({ @@ -169,7 +169,7 @@ describe('Upgrade Assistant Telemetry API', () => { const resp = await routeDependencies.router.getHandler({ method: 'put', - pathPattern: '/api/upgrade_assistant/telemetry/ui_reindex', + pathPattern: '/api/upgrade_assistant/stats/ui_reindex', })( routeHandlerContextMock, createRequestMock({ diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts index 900a5e64c55c3..71f5de01f6a44 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../types'; export function registerTelemetryRoutes({ router, getSavedObjectsService }: RouteDependencies) { router.put( { - path: '/api/upgrade_assistant/telemetry/ui_open', + path: '/api/upgrade_assistant/stats/ui_open', validate: { body: schema.object({ overview: schema.boolean({ defaultValue: false }), @@ -40,7 +40,7 @@ export function registerTelemetryRoutes({ router, getSavedObjectsService }: Rout router.put( { - path: '/api/upgrade_assistant/telemetry/ui_reindex', + path: '/api/upgrade_assistant/stats/ui_reindex', validate: { body: schema.object({ close: schema.boolean({ defaultValue: false }), From 6d819b7a1d95e8ecc50eef364ef1448544bf21da Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 22 Sep 2020 08:46:00 -0400 Subject: [PATCH 02/92] [Ingest Manager] Fix agent action acknowledgement (#78089) --- .../server/services/agents/acks.test.ts | 110 +++++++++++++++++- .../server/services/agents/acks.ts | 8 +- 2 files changed, 111 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts index 866aa587b8a56..c7b4098803827 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts @@ -57,7 +57,7 @@ describe('test agent acks services', () => { ); }); - it('should update config field on the agent if a policy change is acknowledged', async () => { + it('should update config field on the agent if a policy change is acknowledged with an agent without policy', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); const actionAttributes = { @@ -116,6 +116,114 @@ describe('test agent acks services', () => { `); }); + it('should update config field on the agent if a policy change is acknowledged with a higher revision than the agent one', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + + const actionAttributes = { + type: 'CONFIG_CHANGE', + policy_id: 'policy1', + policy_revision: 4, + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + ack_data: JSON.stringify({ packages: ['system'] }), + }; + + mockSavedObjectsClient.bulkGet.mockReturnValue( + Promise.resolve({ + saved_objects: [ + { + id: 'action2', + references: [], + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + attributes: actionAttributes, + }, + ], + } as SavedObjectsBulkResponse) + ); + + await acknowledgeAgentActions( + mockSavedObjectsClient, + ({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + policy_id: 'policy1', + policy_revision: 3, + } as unknown) as Agent, + [ + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: 'action2', + agent_id: 'id', + } as AgentEvent, + ] + ); + expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); + expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(1); + expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0][0]).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "packages": Array [ + "system", + ], + "policy_revision": 4, + }, + "id": "id", + "type": "fleet-agents", + } + `); + }); + + it('should not update config field on the agent if a policy change is acknowledged with a lower revision than the agent one', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + + const actionAttributes = { + type: 'CONFIG_CHANGE', + policy_id: 'policy1', + policy_revision: 4, + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + ack_data: JSON.stringify({ packages: ['system'] }), + }; + + mockSavedObjectsClient.bulkGet.mockReturnValue( + Promise.resolve({ + saved_objects: [ + { + id: 'action2', + references: [], + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + attributes: actionAttributes, + }, + ], + } as SavedObjectsBulkResponse) + ); + + await acknowledgeAgentActions( + mockSavedObjectsClient, + ({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + policy_id: 'policy1', + policy_revision: 5, + } as unknown) as Agent, + [ + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: 'action2', + agent_id: 'id', + } as AgentEvent, + ] + ); + expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); + expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(0); + }); + it('should not update config field on the agent if a policy change for an old revision is acknowledged', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts index d29dfcec7ef30..1392710eb0eff 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts @@ -139,16 +139,12 @@ function getLatestConfigChangePolicyActionIfUpdated( !isAgentPolicyAction(action) || action.type !== 'CONFIG_CHANGE' || action.policy_id !== agent.policy_id || - (acc?.policy_revision ?? 0) < (agent.policy_revision || 0) + (action?.policy_revision ?? 0) < (agent.policy_revision || 0) ) { return acc; } - if (action.policy_revision > (acc?.policy_revision ?? 0)) { - return action; - } - - return acc; + return action; }, null); } From ef86fbc7802008f3a9cee8ef609d964749d15e3a Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 22 Sep 2020 15:31:07 +0200 Subject: [PATCH 03/92] call .destroy on ace when react component unmounts (#78132) --- .../containers/editor/legacy/console_editor/editor.tsx | 3 +++ .../models/legacy_core_editor/legacy_core_editor.ts | 4 ++++ src/plugins/console/public/types/core_editor.ts | 5 +++++ .../searchprofiler/public/application/editor/editor.tsx | 6 ++++++ 4 files changed, 18 insertions(+) diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index fc88b31711b23..abef8afcc3985 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -182,6 +182,9 @@ function EditorUI({ initialTextValue }: EditorProps) { unsubscribeResizer(); clearSubscriptions(); window.removeEventListener('hashchange', onHashChange); + if (editorInstanceRef.current) { + editorInstanceRef.current.getCoreEditor().destroy(); + } }; }, [saveCurrentTextObject, initialTextValue, history, setInputEditor, settingsService]); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts index 469ef6d79fae5..393b7eee346f5 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts +++ b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts @@ -408,4 +408,8 @@ export class LegacyCoreEditor implements CoreEditor { }, ]); } + + destroy() { + this.editor.destroy(); + } } diff --git a/src/plugins/console/public/types/core_editor.ts b/src/plugins/console/public/types/core_editor.ts index b71f4fff44ca5..d88d8f86b874c 100644 --- a/src/plugins/console/public/types/core_editor.ts +++ b/src/plugins/console/public/types/core_editor.ts @@ -268,4 +268,9 @@ export interface CoreEditor { * detects a change */ registerAutocompleter(autocompleter: AutoCompleterFunction): void; + + /** + * Release any resources in use by the editor. + */ + destroy(): void; } diff --git a/x-pack/plugins/searchprofiler/public/application/editor/editor.tsx b/x-pack/plugins/searchprofiler/public/application/editor/editor.tsx index 3141f5bedc8f9..7e7d74155b2d9 100644 --- a/x-pack/plugins/searchprofiler/public/application/editor/editor.tsx +++ b/x-pack/plugins/searchprofiler/public/application/editor/editor.tsx @@ -56,6 +56,12 @@ export const Editor = memo(({ licenseEnabled, initialValue, onEditorReady }: Pro setTextArea(licenseEnabled ? containerRef.current!.querySelector('textarea') : null); onEditorReady(createEditorShim(editorInstanceRef.current)); + + return () => { + if (editorInstanceRef.current) { + editorInstanceRef.current.destroy(); + } + }; }, [initialValue, onEditorReady, licenseEnabled]); return ( From 3f5243eefae435532b25f4dcf8bdac29f2244944 Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Tue, 22 Sep 2020 10:15:27 -0400 Subject: [PATCH 04/92] [Alerting] optimize calculation of unmuted alert instances (#78021) This PR optimizes the calculation of instances which should be executed, by optimizing the way the muted instances are removed from the collection of triggered instances. --- x-pack/plugins/alerts/server/task_runner/task_runner.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 5be684eca4651..7ea3f83d747c0 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pickBy, mapValues, omit, without } from 'lodash'; +import { pickBy, mapValues, without } from 'lodash'; import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance } from '../../../task_manager/server'; @@ -228,12 +228,13 @@ export class TaskRunner { }); if (!muteAll) { - const enabledAlertInstances = omit(instancesWithScheduledActions, ...mutedInstanceIds); + const mutedInstanceIdsSet = new Set(mutedInstanceIds); await Promise.all( - Object.entries(enabledAlertInstances) + Object.entries(instancesWithScheduledActions) .filter( - ([, alertInstance]: [string, AlertInstance]) => !alertInstance.isThrottled(throttle) + ([alertInstanceName, alertInstance]: [string, AlertInstance]) => + !alertInstance.isThrottled(throttle) && !mutedInstanceIdsSet.has(alertInstanceName) ) .map(([id, alertInstance]: [string, AlertInstance]) => this.executeAlertInstance(id, alertInstance, executionHandler) From a49b99011515f10e8d1e788bbd16d037a47428cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Tue, 22 Sep 2020 15:17:40 +0100 Subject: [PATCH 05/92] [Enterprise Search] Rename "telemetry" to "stats" (#78124) --- .../applications/shared/telemetry/send_telemetry.test.tsx | 8 ++++---- .../applications/shared/telemetry/send_telemetry.tsx | 2 +- .../server/routes/enterprise_search/telemetry.test.ts | 2 +- .../server/routes/enterprise_search/telemetry.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx index 8f7cf090e2d57..1d64b453b2c2c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx @@ -33,7 +33,7 @@ describe('Shared Telemetry Helpers', () => { metric: 'setup_guide', }); - expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', { + expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { headers, body: '{"product":"enterprise_search","action":"viewed","metric":"setup_guide"}', }); @@ -54,7 +54,7 @@ describe('Shared Telemetry Helpers', () => { http: httpMock, }); - expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', { + expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { headers, body: '{"product":"enterprise_search","action":"viewed","metric":"page"}', }); @@ -65,7 +65,7 @@ describe('Shared Telemetry Helpers', () => { http: httpMock, }); - expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', { + expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { headers, body: '{"product":"app_search","action":"clicked","metric":"button"}', }); @@ -76,7 +76,7 @@ describe('Shared Telemetry Helpers', () => { http: httpMock, }); - expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', { + expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { headers, body: '{"product":"workplace_search","action":"error","metric":"not_found"}', }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx index 4df1428221de6..e3c9ba9b8a218 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx @@ -27,7 +27,7 @@ interface ISendTelemetry extends ISendTelemetryProps { export const sendTelemetry = async ({ http, product, action, metric }: ISendTelemetry) => { try { const body = JSON.stringify({ product, action, metric }); - await http.put('/api/enterprise_search/telemetry', { headers, body }); + await http.put('/api/enterprise_search/stats', { headers, body }); } catch (error) { throw new Error('Unable to send telemetry'); } diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts index acddd3539965a..bd6f4b9da91fd 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts @@ -35,7 +35,7 @@ describe('Enterprise Search Telemetry API', () => { }); }); - describe('PUT /api/enterprise_search/telemetry', () => { + describe('PUT /api/enterprise_search/stats', () => { it('increments the saved objects counter for App Search', async () => { (incrementUICounter as jest.Mock).mockImplementation(jest.fn(() => successResponse)); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts index bfc07c8b64ef5..8f6638ddc099e 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts @@ -25,7 +25,7 @@ export function registerTelemetryRoute({ }: IRouteDependencies) { router.put( { - path: '/api/enterprise_search/telemetry', + path: '/api/enterprise_search/stats', validate: { body: schema.object({ product: schema.oneOf([ From 037eac55902de5d1e40d3372e83897384f4e95b0 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Tue, 22 Sep 2020 10:14:31 -0500 Subject: [PATCH 06/92] Remove service map beta badge (#78039) Fixes #60529. --- docs/apm/service-maps.asciidoc | 5 --- .../components/app/ServiceMap/BetaBadge.tsx | 36 ------------------- .../components/app/ServiceMap/index.tsx | 10 +++--- .../translations/translations/ja-JP.json | 2 -- .../translations/translations/zh-CN.json | 2 -- 5 files changed, 4 insertions(+), 51 deletions(-) delete mode 100644 x-pack/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc index db2f85c54c762..d629a95073a74 100644 --- a/docs/apm/service-maps.asciidoc +++ b/docs/apm/service-maps.asciidoc @@ -2,11 +2,6 @@ [[service-maps]] === Service maps -beta::[] - -WARNING: Service map support for Internet Explorer 11 is extremely limited. -Please use Chrome or Firefox if available. - A service map is a real-time visual representation of the instrumented services in your application's architecture. It shows you how these services are connected, along with high-level metrics like average transaction duration, requests per minute, and errors per minute. diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx deleted file mode 100644 index b468470e3a17d..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx +++ /dev/null @@ -1,36 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiBetaBadge } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import styled from 'styled-components'; - -const BetaBadgeContainer = styled.div` - right: ${({ theme }) => theme.eui.gutterTypes.gutterMedium}; - position: absolute; - top: ${({ theme }) => theme.eui.gutterTypes.gutterSmall}; - z-index: 1; /* The element containing the cytoscape canvas has z-index = 0. */ -`; - -export function BetaBadge() { - return ( - - - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index cb5a57e9ab9fb..bb450131bdfb8 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -4,15 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useTheme } from '../../../hooks/useTheme'; +import React from 'react'; +import { useTrackPageview } from '../../../../../observability/public'; import { invalidLicenseMessage, isActivePlatinumLicense, } from '../../../../common/service_map'; import { useFetcher } from '../../../hooks/useFetcher'; import { useLicense } from '../../../hooks/useLicense'; +import { useTheme } from '../../../hooks/useTheme'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { callApmApi } from '../../../services/rest/createCallApmApi'; import { LicensePrompt } from '../../shared/LicensePrompt'; @@ -22,8 +23,6 @@ import { getCytoscapeDivStyle } from './cytoscapeOptions'; import { EmptyBanner } from './EmptyBanner'; import { Popover } from './Popover'; import { useRefDimensions } from './useRefDimensions'; -import { BetaBadge } from './BetaBadge'; -import { useTrackPageview } from '../../../../../observability/public'; interface ServiceMapProps { serviceName?: string; @@ -80,7 +79,6 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { style={getCytoscapeDivStyle(theme)} > - {serviceName && } @@ -96,7 +94,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { grow={false} style={{ width: 600, textAlign: 'center' as const }} > - + ); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b86d59762c8b8..f626835da8e11 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4806,8 +4806,6 @@ "xpack.apm.serviceMap.avgMemoryUsagePopoverStat": "メモリー使用状況(平均)", "xpack.apm.serviceMap.avgReqPerMinutePopoverMetric": "1分あたりのリクエスト(平均)", "xpack.apm.serviceMap.avgTransDurationPopoverStat": "トランザクションの長さ(平均)", - "xpack.apm.serviceMap.betaBadge": "ベータ", - "xpack.apm.serviceMap.betaTooltipMessage": "現在、この機能はベータです。不具合を見つけた場合やご意見がある場合、サポートに問い合わせるか、またはディスカッションフォーラムにご報告ください。", "xpack.apm.serviceMap.center": "中央", "xpack.apm.serviceMap.download": "ダウンロード", "xpack.apm.serviceMap.emptyBanner.docsLink": "詳細はドキュメントをご覧ください", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 28d9cfa4aaf0d..d6baa87ca9e2f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4809,8 +4809,6 @@ "xpack.apm.serviceMap.avgMemoryUsagePopoverStat": "内存使用率(平均值)", "xpack.apm.serviceMap.avgReqPerMinutePopoverMetric": "每分钟请求数(平均)", "xpack.apm.serviceMap.avgTransDurationPopoverStat": "事务持续时间(平均值)", - "xpack.apm.serviceMap.betaBadge": "公测版", - "xpack.apm.serviceMap.betaTooltipMessage": "此功能当前为公测版。如果遇到任何错误或有任何反馈,请报告问题或访问我们的论坛。", "xpack.apm.serviceMap.center": "中", "xpack.apm.serviceMap.download": "下载", "xpack.apm.serviceMap.emptyBanner.docsLink": "在文档中了解详情", From 99f652479a78a01e4cd29e2333415567f21fdd49 Mon Sep 17 00:00:00 2001 From: Constance Date: Tue, 22 Sep 2020 08:33:06 -0700 Subject: [PATCH 07/92] [Enterprise Search] Fix various plugin states when app has error connecting to Enterprise Search (#78091) * Display error connecting prompt on Overview page instead of blank page * Fix App Search and Workplace Search to not crash during error connecting - due to obj type errors --- .../applications/app_search/app_logic.test.ts | 9 +++++++++ .../applications/app_search/app_logic.ts | 4 ++-- .../error_connecting.test.tsx | 19 +++++++++++++++++++ .../error_connecting/error_connecting.tsx | 18 ++++++++++++++++++ .../components/error_connecting/index.ts | 7 +++++++ .../enterprise_search/index.test.tsx | 17 ++++++++++++++++- .../applications/enterprise_search/index.tsx | 8 +++++++- .../workplace_search/app_logic.test.ts | 10 ++++++++++ .../workplace_search/app_logic.ts | 11 +++++++---- 9 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts index 0f7bfe09edf7e..9410b9ef7cb03 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts @@ -56,6 +56,15 @@ describe('AppLogic', () => { }), }); }); + + it('gracefully handles missing initial data', () => { + AppLogic.actions.initializeAppData({}); + + expect(AppLogic.values).toEqual({ + ...DEFAULT_VALUES, + hasInitialized: true, + }); + }); }); describe('setOnboardingComplete()', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts index 8e5a8d75f407f..932e84af45c2b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -39,7 +39,7 @@ export const AppLogic = kea>({ account: [ {}, { - initializeAppData: (_, { appSearch: account }) => account, + initializeAppData: (_, { appSearch: account }) => account || {}, setOnboardingComplete: (account) => ({ ...account, onboardingComplete: true, @@ -49,7 +49,7 @@ export const AppLogic = kea>({ configuredLimits: [ {}, { - initializeAppData: (_, { configuredLimits }) => configuredLimits.appSearch, + initializeAppData: (_, { configuredLimits }) => configuredLimits?.appSearch || {}, }, ], ilmEnabled: [ diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx new file mode 100644 index 0000000000000..8d48875a8e1f5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ErrorStatePrompt } from '../../../shared/error_state'; +import { ErrorConnecting } from './'; + +describe('ErrorConnecting', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(ErrorStatePrompt)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx new file mode 100644 index 0000000000000..567c77792583d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiPage, EuiPageContent } from '@elastic/eui'; + +import { ErrorStatePrompt } from '../../../shared/error_state'; + +export const ErrorConnecting: React.FC = () => ( + + + + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/index.ts new file mode 100644 index 0000000000000..c8b71e1a6e791 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ErrorConnecting } from './error_connecting'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx index cd2a22a45bbb4..b2918dac086f6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx @@ -6,13 +6,20 @@ import React from 'react'; import { shallow } from 'enzyme'; - import { EuiPage } from '@elastic/eui'; +import '../__mocks__/kea.mock'; +import { useValues } from 'kea'; + import { EnterpriseSearch } from './'; +import { ErrorConnecting } from './components/error_connecting'; import { ProductCard } from './components/product_card'; describe('EnterpriseSearch', () => { + beforeEach(() => { + (useValues as jest.Mock).mockReturnValue({ errorConnecting: false }); + }); + it('renders the overview page and product cards', () => { const wrapper = shallow( @@ -22,6 +29,14 @@ describe('EnterpriseSearch', () => { expect(wrapper.find(ProductCard)).toHaveLength(2); }); + it('renders the error connecting prompt', () => { + (useValues as jest.Mock).mockReturnValueOnce({ errorConnecting: true }); + const wrapper = shallow(); + + expect(wrapper.find(ErrorConnecting)).toHaveLength(1); + expect(wrapper.find(EuiPage)).toHaveLength(0); + }); + describe('access checks', () => { it('does not render the App Search card if the user does not have access to AS', () => { const wrapper = shallow( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx index 373f595a6a9ea..3a3ba02e07058 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx @@ -5,6 +5,7 @@ */ import React from 'react'; +import { useValues } from 'kea'; import { EuiPage, EuiPageBody, @@ -21,9 +22,11 @@ import { i18n } from '@kbn/i18n'; import { IInitialAppData } from '../../../common/types'; import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../common/constants'; +import { HttpLogic } from '../shared/http'; import { SetEnterpriseSearchChrome as SetPageChrome } from '../shared/kibana_chrome'; import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../shared/telemetry'; +import { ErrorConnecting } from './components/error_connecting'; import { ProductCard } from './components/product_card'; import AppSearchImage from './assets/app_search.png'; @@ -31,9 +34,12 @@ import WorkplaceSearchImage from './assets/workplace_search.png'; import './index.scss'; export const EnterpriseSearch: React.FC = ({ access = {} }) => { + const { errorConnecting } = useValues(HttpLogic); const { hasAppSearchAccess, hasWorkplaceSearchAccess } = access; - return ( + return errorConnecting ? ( + + ) : ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts index c52eceb2d2fdd..974e07069ddba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts @@ -50,5 +50,15 @@ describe('AppLogic', () => { expect(AppLogic.values).toEqual(expectedLogicValues); }); + + it('gracefully handles missing initial data', () => { + AppLogic.actions.initializeAppData({}); + + expect(AppLogic.values).toEqual({ + ...DEFAULT_VALUES, + hasInitialized: true, + isFederatedAuth: false, + }); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts index 94bd1d529b65f..629d1969a8f59 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts @@ -21,6 +21,9 @@ export interface IAppActions { initializeAppData(props: IInitialAppData): IInitialAppData; } +const emptyOrg = {} as IOrganization; +const emptyAccount = {} as IAccount; + export const AppLogic = kea>({ path: ['enterprise_search', 'workplace_search', 'app_logic'], actions: { @@ -43,15 +46,15 @@ export const AppLogic = kea>({ }, ], organization: [ - {} as IOrganization, + emptyOrg, { - initializeAppData: (_, { workplaceSearch }) => workplaceSearch!.organization, + initializeAppData: (_, { workplaceSearch }) => workplaceSearch?.organization || emptyOrg, }, ], account: [ - {} as IAccount, + emptyAccount, { - initializeAppData: (_, { workplaceSearch }) => workplaceSearch!.account, + initializeAppData: (_, { workplaceSearch }) => workplaceSearch?.account || emptyAccount, }, ], }, From 7544a33901fe8ec084e937a77a44784290fb83b5 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 22 Sep 2020 18:00:19 +0200 Subject: [PATCH 08/92] [CSM] Use stacked chart for page views (#78042) --- .../RumDashboard/Charts/PageViewsChart.tsx | 25 +- .../lib/rum_client/get_page_view_trends.ts | 63 ++-- .../tests/csm/__snapshots__/page_views.snap | 280 ++++++++++++++++++ .../trial/tests/csm/page_views.ts | 65 ++++ .../apm_api_integration/trial/tests/index.ts | 1 + 5 files changed, 403 insertions(+), 31 deletions(-) create mode 100644 x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap create mode 100644 x-pack/test/apm_api_integration/trial/tests/csm/page_views.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx index c76be19edfe47..904144dec6de9 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx @@ -33,7 +33,10 @@ import { ChartWrapper } from '../ChartWrapper'; import { I18LABELS } from '../translations'; interface Props { - data?: Array>; + data?: { + topItems: string[]; + items: Array>; + }; loading: boolean; } @@ -68,15 +71,9 @@ export function PageViewsChart({ data, loading }: Props) { }); }; - let breakdownAccessors: Set = new Set(); - if (data && data.length > 0) { - data.forEach((item) => { - breakdownAccessors = new Set([ - ...Array.from(breakdownAccessors), - ...Object.keys(item).filter((key) => key !== 'x'), - ]); - }); - } + const breakdownAccessors = data?.topItems?.length ? data?.topItems : ['y']; + + const [darkMode] = useUiSetting$('theme:darkMode'); const customSeriesNaming: SeriesNameFn = ({ yAccessor }) => { if (yAccessor === 'y') { @@ -86,8 +83,6 @@ export function PageViewsChart({ data, loading }: Props) { return yAccessor; }; - const [darkMode] = useUiSetting$('theme:darkMode'); - return ( {(!loading || data) && ( @@ -115,7 +110,8 @@ export function PageViewsChart({ data, loading }: Props) { id="page_views" title={I18LABELS.pageViews} position={Position.Left} - tickFormat={(d) => numeral(d).format('0a')} + tickFormat={(d) => numeral(d).format('0')} + labelFormat={(d) => numeral(d).format('0a')} /> diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts index f25062c67f87a..543aa911b0b1f 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -51,6 +51,16 @@ export async function getPageViewTrends({ } : undefined, }, + ...(breakdownItem + ? { + topBreakdowns: { + terms: { + field: breakdownItem.fieldName, + size: 9, + }, + }, + } + : {}), }, }, }); @@ -59,25 +69,44 @@ export async function getPageViewTrends({ const response = await apmEventClient.search(params); + const { topBreakdowns } = response.aggregations ?? {}; + + // we are only displaying top 9 + const topItems: string[] = (topBreakdowns?.buckets ?? []).map( + ({ key }) => key as string + ); + const result = response.aggregations?.pageViews.buckets ?? []; - return result.map((bucket) => { - const { key: xVal, doc_count: bCount } = bucket; - const res: Record = { - x: xVal, - y: bCount, - }; - if ('breakdown' in bucket) { - const categoryBuckets = bucket.breakdown.buckets; - categoryBuckets.forEach(({ key, doc_count: docCount }) => { - if (key === 'Other') { - res[key + `(${breakdownItem?.name})`] = docCount; - } else { - res[key] = docCount; + return { + topItems, + items: result.map((bucket) => { + const { key: xVal, doc_count: bCount } = bucket; + const res: Record = { + x: xVal, + y: bCount, + }; + if ('breakdown' in bucket) { + let top9Count = 0; + const categoryBuckets = bucket.breakdown.buckets; + categoryBuckets.forEach(({ key, doc_count: docCount }) => { + if (topItems.includes(key as string)) { + if (res[key]) { + // if term is already in object, just add it to it + res[key] += docCount; + } else { + res[key] = docCount; + } + top9Count += docCount; + } + }); + // Top 9 plus others, get a diff from parent bucket total + if (bCount > top9Count) { + res.Other = bCount - top9Count; } - }); - } + } - return res; - }); + return res; + }), + }; } diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap b/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap new file mode 100644 index 0000000000000..38b009fc73d34 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap @@ -0,0 +1,280 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CSM page views when there is data returns page views 1`] = ` +Object { + "items": Array [ + Object { + "x": 1600149947000, + "y": 1, + }, + Object { + "x": 1600149957000, + "y": 0, + }, + Object { + "x": 1600149967000, + "y": 0, + }, + Object { + "x": 1600149977000, + "y": 0, + }, + Object { + "x": 1600149987000, + "y": 0, + }, + Object { + "x": 1600149997000, + "y": 0, + }, + Object { + "x": 1600150007000, + "y": 0, + }, + Object { + "x": 1600150017000, + "y": 0, + }, + Object { + "x": 1600150027000, + "y": 1, + }, + Object { + "x": 1600150037000, + "y": 0, + }, + Object { + "x": 1600150047000, + "y": 0, + }, + Object { + "x": 1600150057000, + "y": 0, + }, + Object { + "x": 1600150067000, + "y": 0, + }, + Object { + "x": 1600150077000, + "y": 1, + }, + Object { + "x": 1600150087000, + "y": 0, + }, + Object { + "x": 1600150097000, + "y": 0, + }, + Object { + "x": 1600150107000, + "y": 0, + }, + Object { + "x": 1600150117000, + "y": 0, + }, + Object { + "x": 1600150127000, + "y": 0, + }, + Object { + "x": 1600150137000, + "y": 0, + }, + Object { + "x": 1600150147000, + "y": 0, + }, + Object { + "x": 1600150157000, + "y": 0, + }, + Object { + "x": 1600150167000, + "y": 0, + }, + Object { + "x": 1600150177000, + "y": 1, + }, + Object { + "x": 1600150187000, + "y": 0, + }, + Object { + "x": 1600150197000, + "y": 0, + }, + Object { + "x": 1600150207000, + "y": 1, + }, + Object { + "x": 1600150217000, + "y": 0, + }, + Object { + "x": 1600150227000, + "y": 0, + }, + Object { + "x": 1600150237000, + "y": 1, + }, + ], + "topItems": Array [], +} +`; + +exports[`CSM page views when there is data returns page views with breakdown 1`] = ` +Object { + "items": Array [ + Object { + "Chrome": 1, + "x": 1600149947000, + "y": 1, + }, + Object { + "x": 1600149957000, + "y": 0, + }, + Object { + "x": 1600149967000, + "y": 0, + }, + Object { + "x": 1600149977000, + "y": 0, + }, + Object { + "x": 1600149987000, + "y": 0, + }, + Object { + "x": 1600149997000, + "y": 0, + }, + Object { + "x": 1600150007000, + "y": 0, + }, + Object { + "x": 1600150017000, + "y": 0, + }, + Object { + "Chrome": 1, + "x": 1600150027000, + "y": 1, + }, + Object { + "x": 1600150037000, + "y": 0, + }, + Object { + "x": 1600150047000, + "y": 0, + }, + Object { + "x": 1600150057000, + "y": 0, + }, + Object { + "x": 1600150067000, + "y": 0, + }, + Object { + "Chrome": 1, + "x": 1600150077000, + "y": 1, + }, + Object { + "x": 1600150087000, + "y": 0, + }, + Object { + "x": 1600150097000, + "y": 0, + }, + Object { + "x": 1600150107000, + "y": 0, + }, + Object { + "x": 1600150117000, + "y": 0, + }, + Object { + "x": 1600150127000, + "y": 0, + }, + Object { + "x": 1600150137000, + "y": 0, + }, + Object { + "x": 1600150147000, + "y": 0, + }, + Object { + "x": 1600150157000, + "y": 0, + }, + Object { + "x": 1600150167000, + "y": 0, + }, + Object { + "Chrome": 1, + "x": 1600150177000, + "y": 1, + }, + Object { + "x": 1600150187000, + "y": 0, + }, + Object { + "x": 1600150197000, + "y": 0, + }, + Object { + "Chrome Mobile": 1, + "x": 1600150207000, + "y": 1, + }, + Object { + "x": 1600150217000, + "y": 0, + }, + Object { + "x": 1600150227000, + "y": 0, + }, + Object { + "Chrome Mobile": 1, + "x": 1600150237000, + "y": 1, + }, + ], + "topItems": Array [ + "Chrome", + "Chrome Mobile", + ], +} +`; + +exports[`CSM page views when there is no data returns empty list 1`] = ` +Object { + "items": Array [], + "topItems": Array [], +} +`; + +exports[`CSM page views when there is no data returns empty list with breakdowns 1`] = ` +Object { + "items": Array [], + "topItems": Array [], +} +`; diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/page_views.ts b/x-pack/test/apm_api_integration/trial/tests/csm/page_views.ts new file mode 100644 index 0000000000000..ca5670d41d8ee --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/csm/page_views.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function rumServicesApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('CSM page views', () => { + describe('when there is no data', () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D' + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatch(); + }); + it('returns empty list with breakdowns', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D&breakdowns=%7B%22name%22%3A%22Browser%22%2C%22fieldName%22%3A%22user_agent.name%22%2C%22type%22%3A%22category%22%7D' + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatch(); + }); + }); + + describe('when there is data', () => { + before(async () => { + await esArchiver.load('8.0.0'); + await esArchiver.load('rum_8.0.0'); + }); + after(async () => { + await esArchiver.unload('8.0.0'); + await esArchiver.unload('rum_8.0.0'); + }); + + it('returns page views', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatch(); + }); + it('returns page views with breakdown', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D&breakdowns=%7B%22name%22%3A%22Browser%22%2C%22fieldName%22%3A%22user_agent.name%22%2C%22type%22%3A%22category%22%7D' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatch(); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index ae62253c62d81..a026f91a02cd7 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -35,6 +35,7 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr loadTestFile(require.resolve('./csm/csm_services.ts')); loadTestFile(require.resolve('./csm/web_core_vitals.ts')); loadTestFile(require.resolve('./csm/long_task_metrics.ts')); + loadTestFile(require.resolve('./csm/page_views.ts')); }); }); } From d666038c8f6767f6790cb78f29e0027ff73d83ee Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 22 Sep 2020 12:40:38 -0400 Subject: [PATCH 09/92] Change saved objects client `find` to allow partial authorization (#77699) --- ...gin-core-public.savedobjectsfindoptions.md | 1 + ...dobjectsfindoptions.typetonamespacesmap.md | 13 ++ ...gin-core-server.savedobjectsfindoptions.md | 1 + ...dobjectsfindoptions.typetonamespacesmap.md | 13 ++ ...core-server.savedobjectsrepository.find.md | 4 +- ...ugin-core-server.savedobjectsrepository.md | 2 +- ...vedobjectsutils.createemptyfindresponse.md | 13 ++ ...na-plugin-core-server.savedobjectsutils.md | 1 + src/core/public/public.api.md | 1 + .../saved_objects/saved_objects_client.ts | 2 +- .../service/lib/repository.test.js | 93 +++++++-- .../saved_objects/service/lib/repository.ts | 65 ++++--- .../lib/search_dsl/query_params.test.ts | 99 ++++++---- .../service/lib/search_dsl/query_params.ts | 25 ++- .../service/lib/search_dsl/search_dsl.test.ts | 4 +- .../service/lib/search_dsl/search_dsl.ts | 3 + .../saved_objects/service/lib/utils.test.ts | 25 ++- .../server/saved_objects/service/lib/utils.ts | 18 ++ src/core/server/saved_objects/types.ts | 8 + src/core/server/server.api.md | 4 +- ...ecure_saved_objects_client_wrapper.test.ts | 75 ++++++- .../secure_saved_objects_client_wrapper.ts | 184 +++++++++++++----- .../server/lib/spaces_client/spaces_client.ts | 2 +- .../spaces_saved_objects_client.test.ts | 30 +++ .../spaces_saved_objects_client.ts | 37 ++-- .../common/lib/saved_object_test_cases.ts | 54 ++++- .../common/lib/saved_object_test_utils.ts | 45 ++--- .../common/lib/types.ts | 1 + .../common/suites/bulk_create.ts | 16 +- .../common/suites/bulk_get.ts | 5 +- .../common/suites/bulk_update.ts | 5 +- .../common/suites/create.ts | 13 +- .../common/suites/delete.ts | 5 +- .../common/suites/export.ts | 61 +++--- .../common/suites/find.ts | 166 ++++++---------- .../common/suites/get.ts | 2 +- .../common/suites/import.ts | 2 +- .../common/suites/resolve_import_errors.ts | 2 +- .../common/suites/update.ts | 5 +- .../security_and_spaces/apis/bulk_create.ts | 36 +++- .../security_and_spaces/apis/create.ts | 32 ++- .../security_and_spaces/apis/export.ts | 32 ++- .../security_and_spaces/apis/find.ts | 119 ++++++----- .../security_only/apis/bulk_create.ts | 28 ++- .../security_only/apis/create.ts | 27 ++- .../security_only/apis/export.ts | 32 ++- .../security_only/apis/find.ts | 44 ++--- .../spaces_only/apis/bulk_create.ts | 68 ++++--- .../spaces_only/apis/create.ts | 50 +++-- .../spaces_only/apis/find.ts | 19 +- 50 files changed, 1067 insertions(+), 525 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.typetonamespacesmap.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.typetonamespacesmap.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.createemptyfindresponse.md diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md index 903462ac3039d..470a41f30afbf 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md @@ -29,4 +29,5 @@ export interface SavedObjectsFindOptions | [sortField](./kibana-plugin-core-public.savedobjectsfindoptions.sortfield.md) | string | | | [sortOrder](./kibana-plugin-core-public.savedobjectsfindoptions.sortorder.md) | string | | | [type](./kibana-plugin-core-public.savedobjectsfindoptions.type.md) | string | string[] | | +| [typeToNamespacesMap](./kibana-plugin-core-public.savedobjectsfindoptions.typetonamespacesmap.md) | Map<string, string[] | undefined> | This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved object client wrapper. If this is defined, it supersedes the type and namespaces fields when building the Elasticsearch query. Any types that are not included in this map will be excluded entirely. If a type is included but its value is undefined, the operation will search for that type in the Default namespace. | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.typetonamespacesmap.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.typetonamespacesmap.md new file mode 100644 index 0000000000000..4af8c9ddeaff4 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.typetonamespacesmap.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) > [typeToNamespacesMap](./kibana-plugin-core-public.savedobjectsfindoptions.typetonamespacesmap.md) + +## SavedObjectsFindOptions.typeToNamespacesMap property + +This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved object client wrapper. If this is defined, it supersedes the `type` and `namespaces` fields when building the Elasticsearch query. Any types that are not included in this map will be excluded entirely. If a type is included but its value is undefined, the operation will search for that type in the Default namespace. + +Signature: + +```typescript +typeToNamespacesMap?: Map; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md index 804c83f7c1b48..ce5c20e60ca11 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md @@ -29,4 +29,5 @@ export interface SavedObjectsFindOptions | [sortField](./kibana-plugin-core-server.savedobjectsfindoptions.sortfield.md) | string | | | [sortOrder](./kibana-plugin-core-server.savedobjectsfindoptions.sortorder.md) | string | | | [type](./kibana-plugin-core-server.savedobjectsfindoptions.type.md) | string | string[] | | +| [typeToNamespacesMap](./kibana-plugin-core-server.savedobjectsfindoptions.typetonamespacesmap.md) | Map<string, string[] | undefined> | This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved object client wrapper. If this is defined, it supersedes the type and namespaces fields when building the Elasticsearch query. Any types that are not included in this map will be excluded entirely. If a type is included but its value is undefined, the operation will search for that type in the Default namespace. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.typetonamespacesmap.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.typetonamespacesmap.md new file mode 100644 index 0000000000000..8bec759f05580 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.typetonamespacesmap.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) > [typeToNamespacesMap](./kibana-plugin-core-server.savedobjectsfindoptions.typetonamespacesmap.md) + +## SavedObjectsFindOptions.typeToNamespacesMap property + +This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved object client wrapper. If this is defined, it supersedes the `type` and `namespaces` fields when building the Elasticsearch query. Any types that are not included in this map will be excluded entirely. If a type is included but its value is undefined, the operation will search for that type in the Default namespace. + +Signature: + +```typescript +typeToNamespacesMap?: Map; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md index 1b562263145da..d3e93e7af2aa0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md @@ -7,14 +7,14 @@ Signature: ```typescript -find({ search, defaultSearchOperator, searchFields, rootSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, }: SavedObjectsFindOptions): Promise>; +find(options: SavedObjectsFindOptions): Promise>; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| { search, defaultSearchOperator, searchFields, rootSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, } | SavedObjectsFindOptions | | +| options | SavedObjectsFindOptions | | Returns: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md index 14d3741425987..1d11d5262a9c4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md @@ -24,7 +24,7 @@ export declare class SavedObjectsRepository | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.delete.md) | | Deletes an object | | [deleteByNamespace(namespace, options)](./kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md) | | Deletes all objects from the provided namespace. | | [deleteFromNamespaces(type, id, namespaces, options)](./kibana-plugin-core-server.savedobjectsrepository.deletefromnamespaces.md) | | Removes one or more namespaces from a given multi-namespace saved object. If no namespaces remain, the saved object is deleted entirely. This method and \[addToNamespaces\][SavedObjectsRepository.addToNamespaces()](./kibana-plugin-core-server.savedobjectsrepository.addtonamespaces.md) are the only ways to change which Spaces a multi-namespace saved object is shared to. | -| [find({ search, defaultSearchOperator, searchFields, rootSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, })](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | | +| [find(options)](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | | | [get(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.get.md) | | Gets a single object | | [incrementCounter(type, id, counterFieldName, options)](./kibana-plugin-core-server.savedobjectsrepository.incrementcounter.md) | | Increases a counter field by one. Creates the document if one doesn't exist for the given id. | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.createemptyfindresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.createemptyfindresponse.md new file mode 100644 index 0000000000000..40e865cb02ce8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.createemptyfindresponse.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUtils](./kibana-plugin-core-server.savedobjectsutils.md) > [createEmptyFindResponse](./kibana-plugin-core-server.savedobjectsutils.createemptyfindresponse.md) + +## SavedObjectsUtils.createEmptyFindResponse property + +Creates an empty response for a find operation. This is only intended to be used by saved objects client wrappers. + +Signature: + +```typescript +static createEmptyFindResponse: ({ page, perPage, }: SavedObjectsFindOptions) => SavedObjectsFindResponse; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md index e365dfbcb5142..83831f65bd41a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md @@ -15,6 +15,7 @@ export declare class SavedObjectsUtils | Property | Modifiers | Type | Description | | --- | --- | --- | --- | +| [createEmptyFindResponse](./kibana-plugin-core-server.savedobjectsutils.createemptyfindresponse.md) | static | <T>({ page, perPage, }: SavedObjectsFindOptions) => SavedObjectsFindResponse<T> | Creates an empty response for a find operation. This is only intended to be used by saved objects client wrappers. | | [namespaceIdToString](./kibana-plugin-core-server.savedobjectsutils.namespaceidtostring.md) | static | (namespace?: string | undefined) => string | Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with the exception of the undefined namespace ID (which has a namespace string of 'default'). | | [namespaceStringToId](./kibana-plugin-core-server.savedobjectsutils.namespacestringtoid.md) | static | (namespace: string) => string | undefined | Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with the exception of the 'default' namespace string (which has a namespace ID of undefined). | diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 1c17be50454c5..7179c6cf8b133 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1079,6 +1079,7 @@ export interface SavedObjectsFindOptions { sortOrder?: string; // (undocumented) type: string | string[]; + typeToNamespacesMap?: Map; } // @public diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index 5a8949ca2f55f..6a10eb44d9ca4 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -34,7 +34,7 @@ import { HttpFetchOptions, HttpSetup } from '../http'; type SavedObjectsFindOptions = Omit< SavedObjectFindOptionsServer, - 'namespace' | 'sortOrder' | 'rootSearchFields' + 'sortOrder' | 'rootSearchFields' | 'typeToNamespacesMap' >; type PromiseType> = T extends Promise ? U : never; diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 352ce4c1c16eb..0e72ad2fec06c 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -2477,6 +2477,33 @@ describe('SavedObjectsRepository', () => { expect(client.search).not.toHaveBeenCalled(); }); + it(`throws when namespaces is an empty array`, async () => { + await expect( + savedObjectsRepository.find({ type: 'foo', namespaces: [] }) + ).rejects.toThrowError('options.namespaces cannot be an empty array'); + expect(client.search).not.toHaveBeenCalled(); + }); + + it(`throws when type is not falsy and typeToNamespacesMap is defined`, async () => { + await expect( + savedObjectsRepository.find({ type: 'foo', typeToNamespacesMap: new Map() }) + ).rejects.toThrowError( + 'options.type must be an empty string when options.typeToNamespacesMap is used' + ); + expect(client.search).not.toHaveBeenCalled(); + }); + + it(`throws when type is not an empty array and typeToNamespacesMap is defined`, async () => { + const test = async (args) => { + await expect(savedObjectsRepository.find(args)).rejects.toThrowError( + 'options.namespaces must be an empty array when options.typeToNamespacesMap is used' + ); + expect(client.search).not.toHaveBeenCalled(); + }; + await test({ type: '', typeToNamespacesMap: new Map() }); + await test({ type: '', namespaces: ['some-ns'], typeToNamespacesMap: new Map() }); + }); + it(`throws when searchFields is defined but not an array`, async () => { await expect( savedObjectsRepository.find({ type, searchFields: 'string' }) @@ -2493,7 +2520,7 @@ describe('SavedObjectsRepository', () => { it(`throws when KQL filter syntax is invalid`, async () => { const findOpts = { - namespace, + namespaces: [namespace], search: 'foo*', searchFields: ['foo'], type: ['dashboard'], @@ -2577,38 +2604,70 @@ describe('SavedObjectsRepository', () => { const test = async (types) => { const result = await savedObjectsRepository.find({ type: types }); expect(result).toEqual(expect.objectContaining({ saved_objects: [] })); + expect(client.search).not.toHaveBeenCalled(); }; await test('unknownType'); await test(HIDDEN_TYPE); await test(['unknownType', HIDDEN_TYPE]); }); + + it(`should return empty results when attempting to find only invalid or hidden types using typeToNamespacesMap`, async () => { + const test = async (types) => { + const result = await savedObjectsRepository.find({ + typeToNamespacesMap: new Map(types.map((x) => [x, undefined])), + type: '', + namespaces: [], + }); + expect(result).toEqual(expect.objectContaining({ saved_objects: [] })); + expect(client.search).not.toHaveBeenCalled(); + }; + + await test(['unknownType']); + await test([HIDDEN_TYPE]); + await test(['unknownType', HIDDEN_TYPE]); + }); }); describe('search dsl', () => { - it(`passes mappings, registry, search, defaultSearchOperator, searchFields, type, sortField, sortOrder and hasReference to getSearchDsl`, async () => { + const commonOptions = { + type: [type], // cannot be used when `typeToNamespacesMap` is present + namespaces: [namespace], // cannot be used when `typeToNamespacesMap` is present + search: 'foo*', + searchFields: ['foo'], + sortField: 'name', + sortOrder: 'desc', + defaultSearchOperator: 'AND', + hasReference: { + type: 'foo', + id: '1', + }, + kueryNode: undefined, + }; + + it(`passes mappings, registry, and search options to getSearchDsl`, async () => { + await findSuccess(commonOptions, namespace); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, commonOptions); + }); + + it(`accepts typeToNamespacesMap`, async () => { const relevantOpts = { - namespaces: [namespace], - search: 'foo*', - searchFields: ['foo'], - type: [type], - sortField: 'name', - sortOrder: 'desc', - defaultSearchOperator: 'AND', - hasReference: { - type: 'foo', - id: '1', - }, - kueryNode: undefined, + ...commonOptions, + type: '', + namespaces: [], + typeToNamespacesMap: new Map([[type, [namespace]]]), // can only be used when `type` is falsy and `namespaces` is an empty array }; await findSuccess(relevantOpts, namespace); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, relevantOpts); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, { + ...relevantOpts, + type: [type], + }); }); it(`accepts KQL expression filter and passes KueryNode to getSearchDsl`, async () => { const findOpts = { - namespace, + namespaces: [namespace], search: 'foo*', searchFields: ['foo'], type: ['dashboard'], @@ -2649,7 +2708,7 @@ describe('SavedObjectsRepository', () => { it(`accepts KQL KueryNode filter and passes KueryNode to getSearchDsl`, async () => { const findOpts = { - namespace, + namespaces: [namespace], search: 'foo*', searchFields: ['foo'], type: ['dashboard'], diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 125f97e7feb11..a83c86e585628 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -67,7 +67,7 @@ import { } from '../../types'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { validateConvertFilterToKueryNode } from './filter_utils'; -import { SavedObjectsUtils } from './utils'; +import { FIND_DEFAULT_PAGE, FIND_DEFAULT_PER_PAGE, SavedObjectsUtils } from './utils'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. @@ -693,37 +693,51 @@ export class SavedObjectsRepository { * @property {string} [options.preference] * @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page } */ - async find({ - search, - defaultSearchOperator = 'OR', - searchFields, - rootSearchFields, - hasReference, - page = 1, - perPage = 20, - sortField, - sortOrder, - fields, - namespaces, - type, - filter, - preference, - }: SavedObjectsFindOptions): Promise> { - if (!type) { + async find(options: SavedObjectsFindOptions): Promise> { + const { + search, + defaultSearchOperator = 'OR', + searchFields, + rootSearchFields, + hasReference, + page = FIND_DEFAULT_PAGE, + perPage = FIND_DEFAULT_PER_PAGE, + sortField, + sortOrder, + fields, + namespaces, + type, + typeToNamespacesMap, + filter, + preference, + } = options; + + if (!type && !typeToNamespacesMap) { throw SavedObjectsErrorHelpers.createBadRequestError( 'options.type must be a string or an array of strings' ); + } else if (namespaces?.length === 0 && !typeToNamespacesMap) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'options.namespaces cannot be an empty array' + ); + } else if (type && typeToNamespacesMap) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'options.type must be an empty string when options.typeToNamespacesMap is used' + ); + } else if ((!namespaces || namespaces?.length) && typeToNamespacesMap) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'options.namespaces must be an empty array when options.typeToNamespacesMap is used' + ); } - const types = Array.isArray(type) ? type : [type]; + const types = type + ? Array.isArray(type) + ? type + : [type] + : Array.from(typeToNamespacesMap!.keys()); const allowedTypes = types.filter((t) => this._allowedTypes.includes(t)); if (allowedTypes.length === 0) { - return { - page, - per_page: perPage, - total: 0, - saved_objects: [], - }; + return SavedObjectsUtils.createEmptyFindResponse(options); } if (searchFields && !Array.isArray(searchFields)) { @@ -766,6 +780,7 @@ export class SavedObjectsRepository { sortField, sortOrder, namespaces, + typeToNamespacesMap, hasReference, kueryNode, }), diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts index 4adc92df31805..e13c67a720400 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts @@ -50,6 +50,40 @@ const ALL_TYPE_SUBSETS = ALL_TYPES.reduce( .filter((x) => x.length) // exclude empty set .map((x) => (x.length === 1 ? x[0] : x)); // if a subset is a single string, destructure it +const createTypeClause = (type: string, namespaces?: string[]) => { + if (registry.isMultiNamespace(type)) { + return { + bool: { + must: expect.arrayContaining([{ terms: { namespaces: namespaces ?? ['default'] } }]), + must_not: [{ exists: { field: 'namespace' } }], + }, + }; + } else if (registry.isSingleNamespace(type)) { + const nonDefaultNamespaces = namespaces?.filter((n) => n !== 'default') ?? []; + const should: any = []; + if (nonDefaultNamespaces.length > 0) { + should.push({ terms: { namespace: nonDefaultNamespaces } }); + } + if (namespaces?.includes('default')) { + should.push({ bool: { must_not: [{ exists: { field: 'namespace' } }] } }); + } + return { + bool: { + must: [{ term: { type } }], + should: expect.arrayContaining(should), + minimum_should_match: 1, + must_not: [{ exists: { field: 'namespaces' } }], + }, + }; + } + // isNamespaceAgnostic + return { + bool: expect.objectContaining({ + must_not: [{ exists: { field: 'namespace' } }, { exists: { field: 'namespaces' } }], + }), + }; +}; + /** * Note: these tests cases are defined in the order they appear in the source code, for readability's sake */ @@ -198,40 +232,6 @@ describe('#getQueryParams', () => { }); describe('`namespaces` parameter', () => { - const createTypeClause = (type: string, namespaces?: string[]) => { - if (registry.isMultiNamespace(type)) { - return { - bool: { - must: expect.arrayContaining([{ terms: { namespaces: namespaces ?? ['default'] } }]), - must_not: [{ exists: { field: 'namespace' } }], - }, - }; - } else if (registry.isSingleNamespace(type)) { - const nonDefaultNamespaces = namespaces?.filter((n) => n !== 'default') ?? []; - const should: any = []; - if (nonDefaultNamespaces.length > 0) { - should.push({ terms: { namespace: nonDefaultNamespaces } }); - } - if (namespaces?.includes('default')) { - should.push({ bool: { must_not: [{ exists: { field: 'namespace' } }] } }); - } - return { - bool: { - must: [{ term: { type } }], - should: expect.arrayContaining(should), - minimum_should_match: 1, - must_not: [{ exists: { field: 'namespaces' } }], - }, - }; - } - // isNamespaceAgnostic - return { - bool: expect.objectContaining({ - must_not: [{ exists: { field: 'namespace' } }, { exists: { field: 'namespaces' } }], - }), - }; - }; - const expectResult = (result: Result, ...typeClauses: any) => { expect(result.query.bool.filter).toEqual( expect.arrayContaining([ @@ -281,6 +281,37 @@ describe('#getQueryParams', () => { test(['default']); }); }); + + describe('`typeToNamespacesMap` parameter', () => { + const expectResult = (result: Result, ...typeClauses: any) => { + expect(result.query.bool.filter).toEqual( + expect.arrayContaining([ + { bool: expect.objectContaining({ should: typeClauses, minimum_should_match: 1 }) }, + ]) + ); + }; + + it('supersedes `type` and `namespaces` parameters', () => { + const result = getQueryParams({ + mappings, + registry, + type: ['pending', 'saved', 'shared', 'global'], + namespaces: ['foo', 'bar', 'default'], + typeToNamespacesMap: new Map([ + ['pending', ['foo']], // 'pending' is only authorized in the 'foo' namespace + // 'saved' is not authorized in any namespaces + ['shared', ['bar', 'default']], // 'shared' is only authorized in the 'bar' and 'default' namespaces + ['global', ['foo', 'bar', 'default']], // 'global' is authorized in all namespaces (which are ignored anyway) + ]), + }); + expectResult( + result, + createTypeClause('pending', ['foo']), + createTypeClause('shared', ['bar', 'default']), + createTypeClause('global') + ); + }); + }); }); describe('search clause (query.bool.must.simple_query_string)', () => { diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index 642d51c70766e..eaddc05fa921c 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -129,6 +129,7 @@ interface QueryParams { registry: ISavedObjectTypeRegistry; namespaces?: string[]; type?: string | string[]; + typeToNamespacesMap?: Map; search?: string; searchFields?: string[]; rootSearchFields?: string[]; @@ -145,6 +146,7 @@ export function getQueryParams({ registry, namespaces, type, + typeToNamespacesMap, search, searchFields, rootSearchFields, @@ -152,7 +154,10 @@ export function getQueryParams({ hasReference, kueryNode, }: QueryParams) { - const types = getTypes(mappings, type); + const types = getTypes( + mappings, + typeToNamespacesMap ? Array.from(typeToNamespacesMap.keys()) : type + ); // A de-duplicated set of namespaces makes for a more effecient query. // @@ -163,9 +168,12 @@ export function getQueryParams({ // since that is consistent with how a single-namespace search behaves in the OSS distribution. Leaving the wildcard in place // would result in no results being returned, as the wildcard is treated as a literal, and not _actually_ as a wildcard. // We had a good discussion around the tradeoffs here: https://github.com/elastic/kibana/pull/67644#discussion_r441055716 - const normalizedNamespaces = namespaces - ? Array.from(new Set(namespaces.map((x) => (x === '*' ? DEFAULT_NAMESPACE_STRING : x)))) - : undefined; + const normalizeNamespaces = (namespacesToNormalize?: string[]) => + namespacesToNormalize + ? Array.from( + new Set(namespacesToNormalize.map((x) => (x === '*' ? DEFAULT_NAMESPACE_STRING : x))) + ) + : undefined; const bool: any = { filter: [ @@ -197,9 +205,12 @@ export function getQueryParams({ }, ] : undefined, - should: types.map((shouldType) => - getClauseForType(registry, normalizedNamespaces, shouldType) - ), + should: types.map((shouldType) => { + const normalizedNamespaces = normalizeNamespaces( + typeToNamespacesMap ? typeToNamespacesMap.get(shouldType) : namespaces + ); + return getClauseForType(registry, normalizedNamespaces, shouldType); + }), minimum_should_match: 1, }, }, diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts index 62e629ad33cc8..7276e505bce7d 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts @@ -57,10 +57,11 @@ describe('getSearchDsl', () => { }); describe('passes control', () => { - it('passes (mappings, schema, namespaces, type, search, searchFields, rootSearchFields, hasReference) to getQueryParams', () => { + it('passes (mappings, schema, namespaces, type, typeToNamespacesMap, search, searchFields, rootSearchFields, hasReference) to getQueryParams', () => { const opts = { namespaces: ['foo-namespace'], type: 'foo', + typeToNamespacesMap: new Map(), search: 'bar', searchFields: ['baz'], rootSearchFields: ['qux'], @@ -78,6 +79,7 @@ describe('getSearchDsl', () => { registry, namespaces: opts.namespaces, type: opts.type, + typeToNamespacesMap: opts.typeToNamespacesMap, search: opts.search, searchFields: opts.searchFields, rootSearchFields: opts.rootSearchFields, diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index aa79a10b2a9be..858770579fb9e 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -35,6 +35,7 @@ interface GetSearchDslOptions { sortField?: string; sortOrder?: string; namespaces?: string[]; + typeToNamespacesMap?: Map; hasReference?: { type: string; id: string; @@ -56,6 +57,7 @@ export function getSearchDsl( sortField, sortOrder, namespaces, + typeToNamespacesMap, hasReference, kueryNode, } = options; @@ -74,6 +76,7 @@ export function getSearchDsl( registry, namespaces, type, + typeToNamespacesMap, search, searchFields, rootSearchFields, diff --git a/src/core/server/saved_objects/service/lib/utils.test.ts b/src/core/server/saved_objects/service/lib/utils.test.ts index ea4fa68242bea..ac06ca9275783 100644 --- a/src/core/server/saved_objects/service/lib/utils.test.ts +++ b/src/core/server/saved_objects/service/lib/utils.test.ts @@ -17,10 +17,11 @@ * under the License. */ +import { SavedObjectsFindOptions } from '../../types'; import { SavedObjectsUtils } from './utils'; describe('SavedObjectsUtils', () => { - const { namespaceIdToString, namespaceStringToId } = SavedObjectsUtils; + const { namespaceIdToString, namespaceStringToId, createEmptyFindResponse } = SavedObjectsUtils; describe('#namespaceIdToString', () => { it('converts `undefined` to default namespace string', () => { @@ -54,4 +55,26 @@ describe('SavedObjectsUtils', () => { test(''); }); }); + + describe('#createEmptyFindResponse', () => { + it('returns expected result', () => { + const options = {} as SavedObjectsFindOptions; + expect(createEmptyFindResponse(options)).toEqual({ + page: 1, + per_page: 20, + total: 0, + saved_objects: [], + }); + }); + + it('handles `page` field', () => { + const options = { page: 42 } as SavedObjectsFindOptions; + expect(createEmptyFindResponse(options).page).toEqual(42); + }); + + it('handles `perPage` field', () => { + const options = { perPage: 42 } as SavedObjectsFindOptions; + expect(createEmptyFindResponse(options).per_page).toEqual(42); + }); + }); }); diff --git a/src/core/server/saved_objects/service/lib/utils.ts b/src/core/server/saved_objects/service/lib/utils.ts index 6101ad57cc401..3efe8614da1d7 100644 --- a/src/core/server/saved_objects/service/lib/utils.ts +++ b/src/core/server/saved_objects/service/lib/utils.ts @@ -17,7 +17,12 @@ * under the License. */ +import { SavedObjectsFindOptions } from '../../types'; +import { SavedObjectsFindResponse } from '..'; + export const DEFAULT_NAMESPACE_STRING = 'default'; +export const FIND_DEFAULT_PAGE = 1; +export const FIND_DEFAULT_PER_PAGE = 20; /** * @public @@ -50,4 +55,17 @@ export class SavedObjectsUtils { return namespace !== DEFAULT_NAMESPACE_STRING ? namespace : undefined; }; + + /** + * Creates an empty response for a find operation. This is only intended to be used by saved objects client wrappers. + */ + public static createEmptyFindResponse = ({ + page = FIND_DEFAULT_PAGE, + perPage = FIND_DEFAULT_PER_PAGE, + }: SavedObjectsFindOptions): SavedObjectsFindResponse => ({ + page, + per_page: perPage, + total: 0, + saved_objects: [], + }); } diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 1885f5ec50139..01128e4f8cf51 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -89,6 +89,14 @@ export interface SavedObjectsFindOptions { defaultSearchOperator?: 'AND' | 'OR'; filter?: string | KueryNode; namespaces?: string[]; + /** + * This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved + * object client wrapper. + * If this is defined, it supersedes the `type` and `namespaces` fields when building the Elasticsearch query. + * Any types that are not included in this map will be excluded entirely. + * If a type is included but its value is undefined, the operation will search for that type in the Default namespace. + */ + typeToNamespacesMap?: Map; /** An optional ES preference value to be used for the query **/ preference?: string; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index d755ef3e1b676..8a764d9bd2f66 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2177,6 +2177,7 @@ export interface SavedObjectsFindOptions { sortOrder?: string; // (undocumented) type: string | string[]; + typeToNamespacesMap?: Map; } // @public @@ -2388,7 +2389,7 @@ export class SavedObjectsRepository { deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise; // (undocumented) - find({ search, defaultSearchOperator, searchFields, rootSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, }: SavedObjectsFindOptions): Promise>; + find(options: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; @@ -2496,6 +2497,7 @@ export interface SavedObjectsUpdateResponse extends Omit({ page, perPage, }: SavedObjectsFindOptions) => SavedObjectsFindResponse; static namespaceIdToString: (namespace?: string | undefined) => string; static namespaceStringToId: (namespace: string) => string | undefined; } diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index 7ada34ff5ccac..86d1b68ba761e 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -609,22 +609,83 @@ describe('#find', () => { await expectGeneralError(client.find, { type: type1 }); }); - test(`throws decorated ForbiddenError when type's singular and unauthorized`, async () => { + test(`returns empty result when unauthorized`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( + getMockCheckPrivilegesFailure + ); + const options = Object.freeze({ type: type1, namespaces: ['some-ns'] }); - await expectForbiddenError(client.find, { options }); - }); + const result = await client.find(options); - test(`throws decorated ForbiddenError when type's an array and unauthorized`, async () => { - const options = Object.freeze({ type: [type1, type2], namespaces: ['some-ns'] }); - await expectForbiddenError(client.find, { options }); + expect(clientOpts.baseClient.find).not.toHaveBeenCalled(); + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(clientOpts.auditLogger.savedObjectsAuthorizationFailure).toHaveBeenCalledWith( + USERNAME, + 'find', + [type1], + options.namespaces, + [{ spaceId: 'some-ns', privilege: 'mock-saved_object:foo/find' }], + { options } + ); + expect(result).toEqual({ page: 1, per_page: 20, total: 0, saved_objects: [] }); }); - test(`returns result of baseClient.find when authorized`, async () => { + test(`returns result of baseClient.find when fully authorized`, async () => { const apiCallReturnValue = { saved_objects: [], foo: 'bar' }; clientOpts.baseClient.find.mockReturnValue(apiCallReturnValue as any); const options = Object.freeze({ type: type1, namespaces: ['some-ns'] }); const result = await expectSuccess(client.find, { options }); + expect(clientOpts.baseClient.find.mock.calls[0][0]).toEqual({ + ...options, + typeToNamespacesMap: undefined, + }); + expect(result).toEqual(apiCallReturnValue); + }); + + test(`returns result of baseClient.find when partially authorized`, async () => { + clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockResolvedValue({ + hasAllRequested: false, + username: USERNAME, + privileges: { + kibana: [ + { resource: 'some-ns', privilege: 'mock-saved_object:foo/find', authorized: true }, + { resource: 'some-ns', privilege: 'mock-saved_object:bar/find', authorized: true }, + { resource: 'some-ns', privilege: 'mock-saved_object:baz/find', authorized: false }, + { resource: 'some-ns', privilege: 'mock-saved_object:qux/find', authorized: false }, + { resource: 'another-ns', privilege: 'mock-saved_object:foo/find', authorized: true }, + { resource: 'another-ns', privilege: 'mock-saved_object:bar/find', authorized: false }, + { resource: 'another-ns', privilege: 'mock-saved_object:baz/find', authorized: true }, + { resource: 'another-ns', privilege: 'mock-saved_object:qux/find', authorized: false }, + { resource: 'forbidden-ns', privilege: 'mock-saved_object:foo/find', authorized: false }, + { resource: 'forbidden-ns', privilege: 'mock-saved_object:bar/find', authorized: false }, + { resource: 'forbidden-ns', privilege: 'mock-saved_object:baz/find', authorized: false }, + { resource: 'forbidden-ns', privilege: 'mock-saved_object:qux/find', authorized: false }, + ], + }, + }); + + const apiCallReturnValue = { saved_objects: [], foo: 'bar' }; + clientOpts.baseClient.find.mockReturnValue(apiCallReturnValue as any); + + const options = Object.freeze({ + type: ['foo', 'bar', 'baz', 'qux'], + namespaces: ['some-ns', 'another-ns', 'forbidden-ns'], + }); + const result = await client.find(options); + // 'expect(clientOpts.baseClient.find).toHaveBeenCalledWith' resulted in false negatives, resorting to manually comparing mock call args + expect(clientOpts.baseClient.find.mock.calls[0][0]).toEqual({ + ...options, + typeToNamespacesMap: new Map([ + ['foo', ['some-ns', 'another-ns']], + ['bar', ['some-ns']], + ['baz', ['another-ns']], + // qux is not authorized, so there is no entry for it + // forbidden-ns is completely forbidden, so there are no entries with this namespace + ]), + type: '', + namespaces: [], + }); expect(result).toEqual(apiCallReturnValue); }); diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 16e52c69f274f..f5de8f4b226f3 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -16,6 +16,7 @@ import { SavedObjectsUpdateOptions, SavedObjectsAddToNamespacesOptions, SavedObjectsDeleteFromNamespacesOptions, + SavedObjectsUtils, } from '../../../../../src/core/server'; import { SecurityAuditLogger } from '../audit'; import { Actions, CheckSavedObjectsPrivileges } from '../authorization'; @@ -39,8 +40,19 @@ interface SavedObjectsNamespaces { saved_objects: SavedObjectNamespaces[]; } -function uniq(arr: T[]): T[] { - return Array.from(new Set(arr)); +interface EnsureAuthorizedOptions { + args?: Record; + auditAction?: string; + requireFullAuthorization?: boolean; +} + +interface EnsureAuthorizedResult { + status: 'fully_authorized' | 'partially_authorized' | 'unauthorized'; + typeMap: Map; +} +interface EnsureAuthorizedTypeResult { + authorizedSpaces: string[]; + isGloballyAuthorized?: boolean; } export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContract { @@ -72,7 +84,8 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra attributes: T = {} as T, options: SavedObjectsCreateOptions = {} ) { - await this.ensureAuthorized(type, 'create', options.namespace, { type, attributes, options }); + const args = { type, attributes, options }; + await this.ensureAuthorized(type, 'create', options.namespace, { args }); const savedObject = await this.baseClient.create(type, attributes, options); return await this.redactSavedObjectNamespaces(savedObject); @@ -82,9 +95,12 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra objects: SavedObjectsCheckConflictsObject[] = [], options: SavedObjectsBaseOptions = {} ) { - const types = this.getUniqueObjectTypes(objects); const args = { objects, options }; - await this.ensureAuthorized(types, 'bulk_create', options.namespace, args, 'checkConflicts'); + const types = this.getUniqueObjectTypes(objects); + await this.ensureAuthorized(types, 'bulk_create', options.namespace, { + args, + auditAction: 'checkConflicts', + }); const response = await this.baseClient.checkConflicts(objects, options); return response; @@ -94,11 +110,12 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra objects: Array>, options: SavedObjectsBaseOptions = {} ) { + const args = { objects, options }; await this.ensureAuthorized( this.getUniqueObjectTypes(objects), 'bulk_create', options.namespace, - { objects, options } + { args } ); const response = await this.baseClient.bulkCreate(objects, options); @@ -106,7 +123,8 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra } public async delete(type: string, id: string, options: SavedObjectsBaseOptions = {}) { - await this.ensureAuthorized(type, 'delete', options.namespace, { type, id, options }); + const args = { type, id, options }; + await this.ensureAuthorized(type, 'delete', options.namespace, { args }); return await this.baseClient.delete(type, id, options); } @@ -121,9 +139,29 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra `_find across namespaces is not permitted when the Spaces plugin is disabled.` ); } - await this.ensureAuthorized(options.type, 'find', options.namespaces, { options }); + const args = { options }; + const { status, typeMap } = await this.ensureAuthorized( + options.type, + 'find', + options.namespaces, + { args, requireFullAuthorization: false } + ); + + if (status === 'unauthorized') { + // return empty response + return SavedObjectsUtils.createEmptyFindResponse(options); + } - const response = await this.baseClient.find(options); + const typeToNamespacesMap = Array.from(typeMap).reduce>( + (acc, [type, { authorizedSpaces, isGloballyAuthorized }]) => + isGloballyAuthorized ? acc.set(type, options.namespaces) : acc.set(type, authorizedSpaces), + new Map() + ); + const response = await this.baseClient.find({ + ...options, + typeToNamespacesMap: undefined, // if the user is fully authorized, use `undefined` as the typeToNamespacesMap to prevent privilege escalation + ...(status === 'partially_authorized' && { typeToNamespacesMap, type: '', namespaces: [] }), // the repository requires that `type` and `namespaces` must be empty if `typeToNamespacesMap` is defined + }); return await this.redactSavedObjectsNamespaces(response); } @@ -131,9 +169,9 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra objects: SavedObjectsBulkGetObject[] = [], options: SavedObjectsBaseOptions = {} ) { + const args = { objects, options }; await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_get', options.namespace, { - objects, - options, + args, }); const response = await this.baseClient.bulkGet(objects, options); @@ -141,7 +179,8 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra } public async get(type: string, id: string, options: SavedObjectsBaseOptions = {}) { - await this.ensureAuthorized(type, 'get', options.namespace, { type, id, options }); + const args = { type, id, options }; + await this.ensureAuthorized(type, 'get', options.namespace, { args }); const savedObject = await this.baseClient.get(type, id, options); return await this.redactSavedObjectNamespaces(savedObject); @@ -154,7 +193,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra options: SavedObjectsUpdateOptions = {} ) { const args = { type, id, attributes, options }; - await this.ensureAuthorized(type, 'update', options.namespace, args); + await this.ensureAuthorized(type, 'update', options.namespace, { args }); const savedObject = await this.baseClient.update(type, id, attributes, options); return await this.redactSavedObjectNamespaces(savedObject); @@ -169,13 +208,19 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra const args = { type, id, namespaces, options }; const { namespace } = options; // To share an object, the user must have the "create" permission in each of the destination namespaces. - await this.ensureAuthorized(type, 'create', namespaces, args, 'addToNamespacesCreate'); + await this.ensureAuthorized(type, 'create', namespaces, { + args, + auditAction: 'addToNamespacesCreate', + }); // To share an object, the user must also have the "update" permission in one or more of the source namespaces. Because the // `addToNamespaces` operation is scoped to the current namespace, we can just check if the user has the "update" permission in the // current namespace. If the user has permission, but the saved object doesn't exist in this namespace, the base client operation will // result in a 404 error. - await this.ensureAuthorized(type, 'update', namespace, args, 'addToNamespacesUpdate'); + await this.ensureAuthorized(type, 'update', namespace, { + args, + auditAction: 'addToNamespacesUpdate', + }); const result = await this.baseClient.addToNamespaces(type, id, namespaces, options); return await this.redactSavedObjectNamespaces(result); @@ -189,7 +234,10 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra ) { const args = { type, id, namespaces, options }; // To un-share an object, the user must have the "delete" permission in each of the target namespaces. - await this.ensureAuthorized(type, 'delete', namespaces, args, 'deleteFromNamespaces'); + await this.ensureAuthorized(type, 'delete', namespaces, { + args, + auditAction: 'deleteFromNamespaces', + }); const result = await this.baseClient.deleteFromNamespaces(type, id, namespaces, options); return await this.redactSavedObjectNamespaces(result); @@ -205,9 +253,9 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra .filter(({ namespace }) => namespace !== undefined) .map(({ namespace }) => namespace!); const namespaces = [options?.namespace, ...objectNamespaces]; + const args = { objects, options }; await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_update', namespaces, { - objects, - options, + args, }); const response = await this.baseClient.bulkUpdate(objects, options); @@ -228,11 +276,10 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra private async ensureAuthorized( typeOrTypes: string | string[], action: string, - namespaceOrNamespaces?: string | Array, - args?: Record, - auditAction: string = action, - requiresAll = true - ) { + namespaceOrNamespaces: undefined | string | Array, + options: EnsureAuthorizedOptions = {} + ): Promise { + const { args, auditAction = action, requireFullAuthorization = true } = options; const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; const actionsToTypesMap = new Map( types.map((type) => [this.actions.savedObject.get(type, action), type]) @@ -245,27 +292,60 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra privileges.kibana.map(({ resource }) => resource).filter((x) => x !== undefined) ).sort() as string[]; - const isAuthorized = - (requiresAll && hasAllRequested) || - (!requiresAll && privileges.kibana.some(({ authorized }) => authorized)); - if (isAuthorized) { - this.auditLogger.savedObjectsAuthorizationSuccess( + const missingPrivileges = this.getMissingPrivileges(privileges); + const typeMap = privileges.kibana.reduce>( + (acc, { resource, privilege, authorized }) => { + if (!authorized) { + return acc; + } + const type = actionsToTypesMap.get(privilege)!; // always defined + const value = acc.get(type) ?? { authorizedSpaces: [] }; + if (resource === undefined) { + return acc.set(type, { ...value, isGloballyAuthorized: true }); + } + const authorizedSpaces = value.authorizedSpaces.concat(resource); + return acc.set(type, { ...value, authorizedSpaces }); + }, + new Map() + ); + + const logAuthorizationFailure = () => { + this.auditLogger.savedObjectsAuthorizationFailure( username, auditAction, types, spaceIds, + missingPrivileges, args ); - } else { - const missingPrivileges = this.getMissingPrivileges(privileges); - this.auditLogger.savedObjectsAuthorizationFailure( + }; + const logAuthorizationSuccess = (typeArray: string[], spaceIdArray: string[]) => { + this.auditLogger.savedObjectsAuthorizationSuccess( username, auditAction, - types, - spaceIds, - missingPrivileges, + typeArray, + spaceIdArray, args ); + }; + + if (hasAllRequested) { + logAuthorizationSuccess(types, spaceIds); + return { typeMap, status: 'fully_authorized' }; + } else if (!requireFullAuthorization) { + const isPartiallyAuthorized = privileges.kibana.some(({ authorized }) => authorized); + if (isPartiallyAuthorized) { + for (const [type, { isGloballyAuthorized, authorizedSpaces }] of typeMap.entries()) { + // generate an individual audit record for each authorized type + logAuthorizationSuccess([type], isGloballyAuthorized ? spaceIds : authorizedSpaces); + } + return { typeMap, status: 'partially_authorized' }; + } else { + logAuthorizationFailure(); + return { typeMap, status: 'unauthorized' }; + } + } else { + logAuthorizationFailure(); const targetTypes = uniq( missingPrivileges.map(({ privilege }) => actionsToTypesMap.get(privilege)).sort() ).join(','); @@ -303,19 +383,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra } private redactAndSortNamespaces(spaceIds: string[], privilegeMap: Record) { - const comparator = (a: string, b: string) => { - const _a = a.toLowerCase(); - const _b = b.toLowerCase(); - if (_a === '?') { - return 1; - } else if (_a < _b) { - return -1; - } else if (_a > _b) { - return 1; - } - return 0; - }; - return spaceIds.map((spaceId) => (privilegeMap[spaceId] ? spaceId : '?')).sort(comparator); + return spaceIds.map((x) => (privilegeMap[x] ? x : '?')).sort(namespaceComparator); } private async redactSavedObjectNamespaces( @@ -362,3 +430,25 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra }; } } + +/** + * Returns all unique elements of an array. + */ +function uniq(arr: T[]): T[] { + return Array.from(new Set(arr)); +} + +/** + * Utility function to sort potentially redacted namespaces. + * Sorts in a case-insensitive manner, and ensures that redacted namespaces ('?') always show up at the end of the array. + */ +function namespaceComparator(a: string, b: string) { + const A = a.toUpperCase(); + const B = b.toUpperCase(); + if (A === '?' && B !== '?') { + return 1; + } else if (A !== '?' && B === '?') { + return -1; + } + return A > B ? 1 : A < B ? -1 : 0; +} diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts index acb00a87bf7d9..5ef0b5375d796 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts @@ -104,7 +104,7 @@ export class SpacesClient { `SpacesClient.getAll(), using RBAC. returning 403/Forbidden. Not authorized for any spaces for ${purpose} purpose.` ); this.auditLogger.spacesAuthorizationFailure(username, 'getAll'); - throw Boom.forbidden(); + throw Boom.forbidden(); // Note: there is a catch for this in `SpacesSavedObjectsClient.find`; if we get rid of this error, remove that too } this.auditLogger.spacesAuthorizationSuccess(username, 'getAll', authorized as string[]); diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts index c9c17d091cd55..f7621f11a1c05 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts @@ -10,6 +10,8 @@ import { spacesServiceMock } from '../spaces_service/spaces_service.mock'; import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { SavedObjectTypeRegistry } from 'src/core/server'; import { SpacesClient } from '../lib/spaces_client'; +import { spacesClientMock } from '../lib/spaces_client/spaces_client.mock'; +import Boom from 'boom'; const typeRegistry = new SavedObjectTypeRegistry(); typeRegistry.registerType({ @@ -129,6 +131,34 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); describe('#find', () => { + const EMPTY_RESPONSE = { saved_objects: [], total: 0, per_page: 20, page: 1 }; + + test(`returns empty result if user is unauthorized in this space`, async () => { + const { client, baseClient, spacesService } = await createSpacesSavedObjectsClient(); + const spacesClient = spacesClientMock.create(); + spacesClient.getAll.mockResolvedValue([]); + spacesService.scopedClient.mockResolvedValue(spacesClient); + + const options = Object.freeze({ type: 'foo', namespaces: ['some-ns'] }); + const actualReturnValue = await client.find(options); + + expect(actualReturnValue).toEqual(EMPTY_RESPONSE); + expect(baseClient.find).not.toHaveBeenCalled(); + }); + + test(`returns empty result if user is unauthorized in any space`, async () => { + const { client, baseClient, spacesService } = await createSpacesSavedObjectsClient(); + const spacesClient = spacesClientMock.create(); + spacesClient.getAll.mockRejectedValue(Boom.unauthorized()); + spacesService.scopedClient.mockResolvedValue(spacesClient); + + const options = Object.freeze({ type: 'foo', namespaces: ['some-ns'] }); + const actualReturnValue = await client.find(options); + + expect(actualReturnValue).toEqual(EMPTY_RESPONSE); + expect(baseClient.find).not.toHaveBeenCalled(); + }); + test(`passes options.type to baseClient if valid singular type specified`, async () => { const { client, baseClient } = await createSpacesSavedObjectsClient(); const expectedReturnValue = { diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts index 4e830d6149537..a65e0431aef92 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import Boom from 'boom'; import { SavedObjectsBaseOptions, SavedObjectsBulkCreateObject, @@ -16,8 +17,9 @@ import { SavedObjectsUpdateOptions, SavedObjectsAddToNamespacesOptions, SavedObjectsDeleteFromNamespacesOptions, + SavedObjectsUtils, ISavedObjectTypeRegistry, -} from 'src/core/server'; +} from '../../../../../src/core/server'; import { SpacesServiceSetup } from '../spaces_service/spaces_service'; import { spaceIdToNamespace } from '../lib/utils/namespace'; import { SpacesClient } from '../lib/spaces_client'; @@ -164,19 +166,26 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { let namespaces = options.namespaces; if (namespaces) { const spacesClient = await this.getSpacesClient; - const availableSpaces = await spacesClient.getAll('findSavedObjects'); - if (namespaces.includes('*')) { - namespaces = availableSpaces.map((space) => space.id); - } else { - namespaces = namespaces.filter((namespace) => - availableSpaces.some((space) => space.id === namespace) - ); - } - // This forbidden error allows this scenario to be consistent - // with the way the SpacesClient behaves when no spaces are authorized - // there. - if (namespaces.length === 0) { - throw this.errors.decorateForbiddenError(new Error()); + + try { + const availableSpaces = await spacesClient.getAll('findSavedObjects'); + if (namespaces.includes('*')) { + namespaces = availableSpaces.map((space) => space.id); + } else { + namespaces = namespaces.filter((namespace) => + availableSpaces.some((space) => space.id === namespace) + ); + } + if (namespaces.length === 0) { + // return empty response, since the user is unauthorized in this space (or these spaces), but we don't return forbidden errors for `find` operations + return SavedObjectsUtils.createEmptyFindResponse(options); + } + } catch (err) { + if (Boom.isBoom(err) && err.output.payload.statusCode === 403) { + // return empty response, since the user is unauthorized in any space, but we don't return forbidden errors for `find` operations + return SavedObjectsUtils.createEmptyFindResponse(options); + } + throw err; } } else { namespaces = [this.spaceId]; diff --git a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_cases.ts b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_cases.ts index b32950538f8e5..190b12e038b27 100644 --- a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_cases.ts +++ b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_cases.ts @@ -4,30 +4,48 @@ * you may not use this file except in compliance with the Elastic License. */ -export const SAVED_OBJECT_TEST_CASES = Object.freeze({ +import { SPACES } from './spaces'; +import { TestCase } from './types'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const EACH_SPACE = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; + +type CommonTestCase = Omit & { originId?: string }; + +export const SAVED_OBJECT_TEST_CASES: Record = Object.freeze({ SINGLE_NAMESPACE_DEFAULT_SPACE: Object.freeze({ type: 'isolatedtype', id: 'defaultspace-isolatedtype-id', + expectedNamespaces: [DEFAULT_SPACE_ID], }), SINGLE_NAMESPACE_SPACE_1: Object.freeze({ type: 'isolatedtype', id: 'space1-isolatedtype-id', + expectedNamespaces: [SPACE_1_ID], }), SINGLE_NAMESPACE_SPACE_2: Object.freeze({ type: 'isolatedtype', id: 'space2-isolatedtype-id', + expectedNamespaces: [SPACE_2_ID], }), MULTI_NAMESPACE_DEFAULT_AND_SPACE_1: Object.freeze({ type: 'sharedtype', id: 'default_and_space_1', + expectedNamespaces: [DEFAULT_SPACE_ID, SPACE_1_ID], }), MULTI_NAMESPACE_ONLY_SPACE_1: Object.freeze({ type: 'sharedtype', id: 'only_space_1', + expectedNamespaces: [SPACE_1_ID], }), MULTI_NAMESPACE_ONLY_SPACE_2: Object.freeze({ type: 'sharedtype', id: 'only_space_2', + expectedNamespaces: [SPACE_2_ID], }), NAMESPACE_AGNOSTIC: Object.freeze({ type: 'globaltype', @@ -38,3 +56,37 @@ export const SAVED_OBJECT_TEST_CASES = Object.freeze({ id: 'any', }), }); + +/** + * These objects exist in the test data for all saved object test suites, but they are only used to test various conflict scenarios. + */ +export const CONFLICT_TEST_CASES: Record = Object.freeze({ + CONFLICT_1_OBJ: Object.freeze({ + type: 'sharedtype', + id: 'conflict_1', + expectedNamespaces: EACH_SPACE, + }), + CONFLICT_2A_OBJ: Object.freeze({ + type: 'sharedtype', + id: 'conflict_2a', + originId: 'conflict_2', + expectedNamespaces: EACH_SPACE, + }), + CONFLICT_2B_OBJ: Object.freeze({ + type: 'sharedtype', + id: 'conflict_2b', + originId: 'conflict_2', + expectedNamespaces: EACH_SPACE, + }), + CONFLICT_3_OBJ: Object.freeze({ + type: 'sharedtype', + id: 'conflict_3', + expectedNamespaces: EACH_SPACE, + }), + CONFLICT_4A_OBJ: Object.freeze({ + type: 'sharedtype', + id: 'conflict_4a', + originId: 'conflict_4', + expectedNamespaces: EACH_SPACE, + }), +}); diff --git a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts index 595986c08efc1..9d4b5e80e9c3d 100644 --- a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts +++ b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts @@ -6,7 +6,6 @@ import expect from '@kbn/expect'; import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; -import { SAVED_OBJECT_TEST_CASES as CASES } from './saved_object_test_cases'; import { SPACES } from './spaces'; import { AUTHENTICATION } from './authentication'; import { TestCase, TestUser, ExpectResponseBody } from './types'; @@ -73,6 +72,28 @@ export const getTestTitle = ( return `${list.join(' and ')}`; }; +export const isUserAuthorizedAtSpace = (user: TestUser | undefined, namespace: string) => + !user || user.authorizedAtSpaces.includes('*') || user.authorizedAtSpaces.includes(namespace); + +export const getRedactedNamespaces = ( + user: TestUser | undefined, + namespaces: string[] | undefined +) => namespaces?.map((x) => (isUserAuthorizedAtSpace(user, x) ? x : '?')).sort(namespaceComparator); +function namespaceComparator(a: string, b: string) { + // namespaces get sorted so that they're all in alphabetical order, and unknown ones appear at the end + // this is to prevent information disclosure + if (a === '?' && b !== '?') { + return 1; + } else if (b === '?' && a !== '?') { + return -1; + } else if (a > b) { + return 1; + } else if (a < b) { + return -1; + } + return 0; +} + export const testCaseFailures = { // test suites need explicit return types for number primitives fail400: (condition?: boolean): { failure?: 400 } => @@ -150,7 +171,7 @@ export const expectResponses = { } }, /** - * Additional assertions that we use in `bulk_create` and `create` to ensure that + * Additional assertions that we use in `import` and `resolve_import_errors` to ensure that * newly-created (or overwritten) objects don't have unexpected properties */ successCreated: async (es: any, spaceId: string, type: string, id: string) => { @@ -161,26 +182,6 @@ export const expectResponses = { id: `${expectedSpacePrefix}${type}:${id}`, index: '.kibana', }); - const { namespace: actualNamespace, namespaces: actualNamespaces } = savedObject._source; - if (isNamespaceUndefined) { - expect(actualNamespace).to.eql(undefined); - } else { - expect(actualNamespace).to.eql(spaceId); - } - if (isMultiNamespace(type)) { - if (['conflict_1', 'conflict_2a', 'conflict_2b', 'conflict_3', 'conflict_4a'].includes(id)) { - expect(actualNamespaces).to.eql([DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]); - } else if (id === CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1.id) { - expect(actualNamespaces).to.eql([DEFAULT_SPACE_ID, SPACE_1_ID]); - } else if (id === CASES.MULTI_NAMESPACE_ONLY_SPACE_1.id) { - expect(actualNamespaces).to.eql([SPACE_1_ID]); - } else if (id === CASES.MULTI_NAMESPACE_ONLY_SPACE_2.id) { - expect(actualNamespaces).to.eql([SPACE_2_ID]); - } else { - // newly created in this space - expect(actualNamespaces).to.eql([spaceId]); - } - } return savedObject; }, }; diff --git a/x-pack/test/saved_object_api_integration/common/lib/types.ts b/x-pack/test/saved_object_api_integration/common/lib/types.ts index 56e6a992b6b62..b52a84f352999 100644 --- a/x-pack/test/saved_object_api_integration/common/lib/types.ts +++ b/x-pack/test/saved_object_api_integration/common/lib/types.ts @@ -21,6 +21,7 @@ export interface TestSuite { export interface TestCase { type: string; id: string; + expectedNamespaces?: string[]; failure?: 400 | 403 | 404 | 409; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts index e3163ef77d427..b1608946b8e62 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts @@ -14,8 +14,9 @@ import { expectResponses, getUrlPrefix, getTestTitle, + getRedactedNamespaces, } from '../lib/saved_object_test_utils'; -import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite, TestUser } from '../lib/types'; export interface BulkCreateTestDefinition extends TestDefinition { request: Array<{ type: string; id: string }>; @@ -33,7 +34,7 @@ const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' }); const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }); const NEW_NAMESPACE_AGNOSTIC_OBJ = Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }); -export const TEST_CASES = Object.freeze({ +export const TEST_CASES: Record = Object.freeze({ ...CASES, NEW_SINGLE_NAMESPACE_OBJ, NEW_MULTI_NAMESPACE_OBJ, @@ -45,7 +46,7 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: const expectResponseBody = ( testCases: BulkCreateTestCase | BulkCreateTestCase[], statusCode: 200 | 403, - spaceId = SPACES.DEFAULT.spaceId + user?: TestUser ): ExpectResponseBody => async (response: Record) => { const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; if (statusCode === 403) { @@ -70,7 +71,8 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: await expectResponses.permitted(object, testCase); if (!testCase.failure) { expect(object.attributes[NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); - await expectResponses.successCreated(es, spaceId, object.type, object.id); + const redactedNamespaces = getRedactedNamespaces(user, testCase.expectedNamespaces); + expect(object.namespaces).to.eql(redactedNamespaces); } } } @@ -81,6 +83,7 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: overwrite: boolean, options?: { spaceId?: string; + user?: TestUser; singleRequest?: boolean; responseBodyOverride?: ExpectResponseBody; } @@ -95,8 +98,7 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: request: [createRequest(x)], responseStatusCode, responseBody: - options?.responseBodyOverride || - expectResponseBody(x, responseStatusCode, options?.spaceId), + options?.responseBodyOverride || expectResponseBody(x, responseStatusCode, options?.user), overwrite, })); } @@ -108,7 +110,7 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: responseStatusCode, responseBody: options?.responseBodyOverride || - expectResponseBody(cases, responseStatusCode, options?.spaceId), + expectResponseBody(cases, responseStatusCode, options?.user), overwrite, }, ]; diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts index 8de54fe499c07..71ece1265347c 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts @@ -25,7 +25,10 @@ export interface BulkGetTestCase extends TestCase { } const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); -export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); +export const TEST_CASES: Record = Object.freeze({ + ...CASES, + DOES_NOT_EXIST, +}); export function bulkGetTestSuiteFactory(esArchiver: any, supertest: SuperTest) { const expectForbidden = expectResponses.forbiddenTypes('bulk_get'); diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts index 2e3c55f029d29..c3020b2da3219 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts @@ -24,7 +24,10 @@ const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, const NEW_ATTRIBUTE_VAL = `Updated attribute value ${Date.now()}`; const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); -export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); +export const TEST_CASES: Record = Object.freeze({ + ...CASES, + DOES_NOT_EXIST, +}); const createRequest = ({ type, id, namespace }: BulkUpdateTestCase) => ({ type, diff --git a/x-pack/test/saved_object_api_integration/common/suites/create.ts b/x-pack/test/saved_object_api_integration/common/suites/create.ts index 2a5ab696c4f53..7e28d5ed9ed94 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/create.ts @@ -13,8 +13,9 @@ import { expectResponses, getUrlPrefix, getTestTitle, + getRedactedNamespaces, } from '../lib/saved_object_test_utils'; -import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite, TestUser } from '../lib/types'; export interface CreateTestDefinition extends TestDefinition { request: { type: string; id: string }; @@ -33,7 +34,7 @@ const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: '' }); const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }); const NEW_NAMESPACE_AGNOSTIC_OBJ = Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }); -export const TEST_CASES = Object.freeze({ +export const TEST_CASES: Record = Object.freeze({ ...CASES, NEW_SINGLE_NAMESPACE_OBJ, NEW_MULTI_NAMESPACE_OBJ, @@ -44,7 +45,7 @@ export function createTestSuiteFactory(es: any, esArchiver: any, supertest: Supe const expectForbidden = expectResponses.forbiddenTypes('create'); const expectResponseBody = ( testCase: CreateTestCase, - spaceId = SPACES.DEFAULT.spaceId + user?: TestUser ): ExpectResponseBody => async (response: Record) => { if (testCase.failure === 403) { await expectForbidden(testCase.type)(response); @@ -54,7 +55,8 @@ export function createTestSuiteFactory(es: any, esArchiver: any, supertest: Supe await expectResponses.permitted(object, testCase); if (!testCase.failure) { expect(object.attributes[NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); - await expectResponses.successCreated(es, spaceId, object.type, object.id); + const redactedNamespaces = getRedactedNamespaces(user, testCase.expectedNamespaces); + expect(object.namespaces).to.eql(redactedNamespaces); } } }; @@ -64,6 +66,7 @@ export function createTestSuiteFactory(es: any, esArchiver: any, supertest: Supe overwrite: boolean, options?: { spaceId?: string; + user?: TestUser; responseBodyOverride?: ExpectResponseBody; } ): CreateTestDefinition[] => { @@ -76,7 +79,7 @@ export function createTestSuiteFactory(es: any, esArchiver: any, supertest: Supe title: getTestTitle(x), responseStatusCode: x.failure ?? 200, request: createRequest(x), - responseBody: options?.responseBodyOverride || expectResponseBody(x, options?.spaceId), + responseBody: options?.responseBodyOverride || expectResponseBody(x, options?.user), overwrite, })); }; diff --git a/x-pack/test/saved_object_api_integration/common/suites/delete.ts b/x-pack/test/saved_object_api_integration/common/suites/delete.ts index 3179b1b0c9ac5..228e7977f99ac 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/delete.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/delete.ts @@ -25,7 +25,10 @@ export interface DeleteTestCase extends TestCase { } const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); -export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); +export const TEST_CASES: Record = Object.freeze({ + ...CASES, + DOES_NOT_EXIST, +}); export function deleteTestSuiteFactory(esArchiver: any, supertest: SuperTest) { const expectForbidden = expectResponses.forbiddenTypes('delete'); diff --git a/x-pack/test/saved_object_api_integration/common/suites/export.ts b/x-pack/test/saved_object_api_integration/common/suites/export.ts index 4a8eff1fb380c..4eb967a952c60 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/export.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/export.ts @@ -30,7 +30,10 @@ export interface ExportTestCase { type: string; id?: string; successResult?: SuccessResult | SuccessResult[]; - failure?: 400 | 403; + failure?: { + statusCode: 200 | 400 | 403; // if the user searches for only types they are not authorized for, they will get an empty 200 result + reason: 'unauthorized' | 'bad_request'; + }; } // additional sharedtype objects that exist but do not have common test cases defined @@ -90,41 +93,45 @@ export const getTestCases = (spaceId?: string): { [key: string]: ExportTestCase type: 'globaltype', successResult: CASES.NAMESPACE_AGNOSTIC, }, - hiddenObject: { title: 'hidden object', ...CASES.HIDDEN, failure: 400 }, - hiddenType: { title: 'hidden type', type: 'hiddentype', failure: 400 }, + hiddenObject: { + title: 'hidden object', + ...CASES.HIDDEN, + failure: { statusCode: 400, reason: 'bad_request' }, + }, + hiddenType: { + title: 'hidden type', + type: 'hiddentype', + failure: { statusCode: 400, reason: 'bad_request' }, + }, }); export const createRequest = ({ type, id }: ExportTestCase) => id ? { objects: [{ type, id }] } : { type }; -const getTestTitle = ({ failure, title }: ExportTestCase) => { - let description = 'success'; - if (failure === 400) { - description = 'bad request'; - } else if (failure === 403) { - description = 'forbidden'; - } - return `${description} ["${title}"]`; -}; +const getTestTitle = ({ failure, title }: ExportTestCase) => + `${failure?.reason || 'success'} ["${title}"]`; + +const EMPTY_RESULT = { exportedCount: 0, missingRefCount: 0, missingReferences: [] }; export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest) { const expectForbiddenBulkGet = expectResponses.forbiddenTypes('bulk_get'); - const expectForbiddenFind = expectResponses.forbiddenTypes('find'); const expectResponseBody = (testCase: ExportTestCase): ExpectResponseBody => async ( response: Record ) => { const { type, id, successResult = { type, id } as SuccessResult, failure } = testCase; - if (failure === 403) { - // In export only, the API uses "bulk_get" or "find" depending on the parameters it receives. - // The best that could be done here is to have an if statement to ensure at least one of the - // two errors has been thrown. - if (id) { + if (failure?.reason === 'unauthorized') { + // In export only, the API uses "bulkGet" or "find" depending on the parameters it receives. + if (failure.statusCode === 403) { + // "bulkGet" was unauthorized, which returns a forbidden error await expectForbiddenBulkGet(type)(response); + } else if (failure.statusCode === 200) { + // "find" was unauthorized, which returns an empty result + expect(response.body).not.to.have.property('error'); + expect(response.text).to.equal(JSON.stringify(EMPTY_RESULT)); } else { - await expectForbiddenFind(type)(response); + throw new Error(`Unexpected failure status code: ${failure.statusCode}`); } - } else if (failure === 400) { - // 400 + } else if (failure?.reason === 'bad_request') { expect(response.body.error).to.eql('Bad Request'); - expect(response.body.statusCode).to.eql(failure); + expect(response.body.statusCode).to.eql(failure.statusCode); if (id) { expect(response.body.message).to.eql( `Trying to export object(s) with non-exportable types: ${type}:${id}` @@ -132,6 +139,8 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest { let cases = Array.isArray(testCases) ? testCases : [testCases]; - if (forbidden) { + if (failure) { // override the expected result in each test case - cases = cases.map((x) => ({ ...x, failure: 403 })); + cases = cases.map((x) => ({ ...x, failure })); } return cases.map((x) => ({ title: getTestTitle(x), - responseStatusCode: x.failure ?? 200, + responseStatusCode: x.failure?.statusCode ?? 200, request: createRequest(x), responseBody: options?.responseBodyOverride || expectResponseBody(x), })); diff --git a/x-pack/test/saved_object_api_integration/common/suites/find.ts b/x-pack/test/saved_object_api_integration/common/suites/find.ts index bab4a4d88534a..381306f810122 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/find.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/find.ts @@ -7,10 +7,13 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; import querystring from 'querystring'; -import { Assign } from '@kbn/utility-types'; -import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SAVED_OBJECT_TEST_CASES, CONFLICT_TEST_CASES } from '../lib/saved_object_test_cases'; import { SPACES } from '../lib/spaces'; -import { expectResponses, getUrlPrefix } from '../lib/saved_object_test_utils'; +import { + getUrlPrefix, + isUserAuthorizedAtSpace, + getRedactedNamespaces, +} from '../lib/saved_object_test_utils'; import { ExpectResponseBody, TestCase, TestDefinition, TestSuite, TestUser } from '../lib/types'; const { @@ -22,80 +25,34 @@ export interface FindTestDefinition extends TestDefinition { } export type FindTestSuite = TestSuite; -type FindSavedObjectCase = Assign; - export interface FindTestCase { title: string; query: string; successResult?: { - savedObjects?: FindSavedObjectCase | FindSavedObjectCase[]; + savedObjects?: TestCase | TestCase[]; page?: number; perPage?: number; total?: number; }; failure?: { - statusCode: 400 | 403; - reason: - | 'forbidden_types' - | 'forbidden_namespaces' - | 'cross_namespace_not_permitted' - | 'bad_request'; + statusCode: 200 | 400; // if the user searches for types and/or namespaces they are not authorized for, they will get a 200 result with those types/namespaces omitted + reason: 'unauthorized' | 'cross_namespace_not_permitted' | 'bad_request'; }; } -// additional sharedtype objects that exist but do not have common test cases defined -const CONFLICT_1_OBJ = Object.freeze({ - type: 'sharedtype', - id: 'conflict_1', - namespaces: ['default', 'space_1', 'space_2'], -}); -const CONFLICT_2A_OBJ = Object.freeze({ - type: 'sharedtype', - id: 'conflict_2a', - originId: 'conflict_2', - namespaces: ['default', 'space_1', 'space_2'], -}); -const CONFLICT_2B_OBJ = Object.freeze({ - type: 'sharedtype', - id: 'conflict_2b', - originId: 'conflict_2', - namespaces: ['default', 'space_1', 'space_2'], -}); -const CONFLICT_3_OBJ = Object.freeze({ - type: 'sharedtype', - id: 'conflict_3', - namespaces: ['default', 'space_1', 'space_2'], -}); -const CONFLICT_4A_OBJ = Object.freeze({ - type: 'sharedtype', - id: 'conflict_4a', - originId: 'conflict_4', - namespaces: ['default', 'space_1', 'space_2'], -}); - const TEST_CASES = [ - { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, namespaces: ['default'] }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, namespaces: ['space_1'] }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, namespaces: ['space_2'] }, - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, namespaces: ['default', 'space_1'] }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, namespaces: ['space_1'] }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, namespaces: ['space_2'] }, - { ...CASES.NAMESPACE_AGNOSTIC, namespaces: undefined }, - { ...CASES.HIDDEN, namespaces: undefined }, + ...Object.values(SAVED_OBJECT_TEST_CASES), + ...Object.values(CONFLICT_TEST_CASES), ]; -expect(TEST_CASES.length).to.eql( - Object.values(CASES).length, - 'Unhandled test cases in `find` suite' -); - export const getTestCases = ( { currentSpace, crossSpaceSearch }: { currentSpace?: string; crossSpaceSearch?: string[] } = { currentSpace: undefined, crossSpaceSearch: undefined, } ) => { - const crossSpaceIds = crossSpaceSearch?.filter((s) => s !== (currentSpace ?? 'default')) ?? []; + const crossSpaceIds = + crossSpaceSearch?.filter((s) => s !== (currentSpace ?? DEFAULT_SPACE_ID)) ?? []; // intentionally exclude the current space const isCrossSpaceSearch = crossSpaceIds.length > 0; const isWildcardSearch = crossSpaceIds.includes('*'); @@ -104,7 +61,7 @@ export const getTestCases = ( : ''; const buildTitle = (title: string) => - crossSpaceSearch ? `${title} (cross-space ${isWildcardSearch ? 'with wildcard' : ''})` : title; + crossSpaceSearch ? `${title} (cross-space${isWildcardSearch ? ' with wildcard' : ''})` : title; type CasePredicate = (testCase: TestCase) => boolean; const getExpectedSavedObjects = (predicate: CasePredicate) => { @@ -117,13 +74,16 @@ export const getTestCases = ( return TEST_CASES.filter((t) => { const hasOtherNamespaces = - Array.isArray(t.namespaces) && - t.namespaces!.some((ns) => ns !== (currentSpace ?? 'default')); + !t.expectedNamespaces || // namespace-agnostic types do not have an expectedNamespaces field + t.expectedNamespaces.some((ns) => ns !== (currentSpace ?? DEFAULT_SPACE_ID)); return hasOtherNamespaces && predicate(t); }); } return TEST_CASES.filter( - (t) => (!t.namespaces || t.namespaces.includes(currentSpace ?? 'default')) && predicate(t) + (t) => + (!t.expectedNamespaces || + t.expectedNamespaces.includes(currentSpace ?? DEFAULT_SPACE_ID)) && + predicate(t) ); }; @@ -140,19 +100,13 @@ export const getTestCases = ( query: `type=sharedtype&fields=title${namespacesQueryParam}`, successResult: { // expected depends on which spaces the user is authorized against... - savedObjects: getExpectedSavedObjects((t) => t.type === 'sharedtype').concat( - CONFLICT_1_OBJ, - CONFLICT_2A_OBJ, - CONFLICT_2B_OBJ, - CONFLICT_3_OBJ, - CONFLICT_4A_OBJ - ), + savedObjects: getExpectedSavedObjects((t) => t.type === 'sharedtype'), }, } as FindTestCase, namespaceAgnosticType: { title: buildTitle('find namespace-agnostic type'), query: `type=globaltype&fields=title${namespacesQueryParam}`, - successResult: { savedObjects: CASES.NAMESPACE_AGNOSTIC }, + successResult: { savedObjects: SAVED_OBJECT_TEST_CASES.NAMESPACE_AGNOSTIC }, } as FindTestCase, hiddenType: { title: buildTitle('find hidden type'), @@ -162,6 +116,15 @@ export const getTestCases = ( title: buildTitle('find unknown type'), query: `type=wigwags${namespacesQueryParam}`, } as FindTestCase, + eachType: { + title: buildTitle('find each type'), + query: `type=isolatedtype&type=sharedtype&type=globaltype&type=hiddentype&type=wigwags${namespacesQueryParam}`, + successResult: { + savedObjects: getExpectedSavedObjects((t) => + ['isolatedtype', 'sharedtype', 'globaltype'].includes(t.type) + ), + }, + } as FindTestCase, pageBeyondTotal: { title: buildTitle('find page beyond total'), query: `type=isolatedtype&page=100&per_page=100${namespacesQueryParam}`, @@ -179,7 +142,7 @@ export const getTestCases = ( filterWithNamespaceAgnosticType: { title: buildTitle('filter with namespace-agnostic type'), query: `type=globaltype&filter=globaltype.attributes.title:*global*${namespacesQueryParam}`, - successResult: { savedObjects: CASES.NAMESPACE_AGNOSTIC }, + successResult: { savedObjects: SAVED_OBJECT_TEST_CASES.NAMESPACE_AGNOSTIC }, } as FindTestCase, filterWithHiddenType: { title: buildTitle('filter with hidden type'), @@ -200,49 +163,48 @@ export const getTestCases = ( }; }; +function objectComparator(a: { id: string }, b: { id: string }) { + return a.id > b.id ? 1 : a.id < b.id ? -1 : 0; +} + export const createRequest = ({ query }: FindTestCase) => ({ query }); -const getTestTitle = ({ failure, title }: FindTestCase) => { - let description = 'success'; - if (failure?.statusCode === 400) { - description = 'bad request'; - } else if (failure?.statusCode === 403) { - description = 'forbidden'; - } - return `${description} ["${title}"]`; -}; +const getTestTitle = ({ failure, title }: FindTestCase) => + `${failure?.reason || 'success'} ["${title}"]`; export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const expectForbiddenTypes = expectResponses.forbiddenTypes('find'); - const expectForbiddeNamespaces = expectResponses.forbiddenSpaces; const expectResponseBody = ( testCase: FindTestCase, user?: TestUser ): ExpectResponseBody => async (response: Record) => { const { failure, successResult = {}, query } = testCase; const parsedQuery = querystring.parse(query); - if (failure?.statusCode === 403) { - if (failure?.reason === 'forbidden_types') { - const type = parsedQuery.type; - await expectForbiddenTypes(type)(response); - } else if (failure?.reason === 'forbidden_namespaces') { - await expectForbiddeNamespaces(response); + if (failure?.statusCode === 200) { + if (failure?.reason === 'unauthorized') { + // if the user is completely unauthorized, they will receive an empty response body + const expected = { + page: parsedQuery.page || 1, + per_page: parsedQuery.per_page || 20, + total: 0, + saved_objects: [], + }; + expect(response.body).to.eql(expected); } else { - throw new Error(`Unexpected failure reason: ${failure?.reason}`); + throw new Error(`Unexpected failure reason: ${failure.reason}`); } } else if (failure?.statusCode === 400) { - if (failure?.reason === 'bad_request') { + if (failure.reason === 'bad_request') { const type = (parsedQuery.filter as string).split('.')[0]; expect(response.body.error).to.eql('Bad Request'); - expect(response.body.statusCode).to.eql(failure?.statusCode); + expect(response.body.statusCode).to.eql(failure.statusCode); expect(response.body.message).to.eql(`This type ${type} is not allowed: Bad Request`); - } else if (failure?.reason === 'cross_namespace_not_permitted') { + } else if (failure.reason === 'cross_namespace_not_permitted') { expect(response.body.error).to.eql('Bad Request'); - expect(response.body.statusCode).to.eql(failure?.statusCode); + expect(response.body.statusCode).to.eql(failure.statusCode); expect(response.body.message).to.eql( `_find across namespaces is not permitted when the Spaces plugin is disabled.: Bad Request` ); } else { - throw new Error(`Unexpected failure reason: ${failure?.reason}`); + throw new Error(`Unexpected failure reason: ${failure.reason}`); } } else { // 2xx @@ -251,11 +213,8 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest) const savedObjectsArray = Array.isArray(savedObjects) ? savedObjects : [savedObjects]; const authorizedSavedObjects = savedObjectsArray.filter( (so) => - !user || - !so.namespaces || - so.namespaces.some( - (ns) => user.authorizedAtSpaces.includes(ns) || user.authorizedAtSpaces.includes('*') - ) + !so.expectedNamespaces || + so.expectedNamespaces.some((x) => isUserAuthorizedAtSpace(user, x)) ); expect(response.body.page).to.eql(page); expect(response.body.per_page).to.eql(perPage); @@ -265,16 +224,17 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest) expect(response.body.total).to.eql(total || authorizedSavedObjects.length); } - authorizedSavedObjects.sort((s1, s2) => (s1.id < s2.id ? -1 : 1)); - response.body.saved_objects.sort((s1: any, s2: any) => (s1.id < s2.id ? -1 : 1)); + authorizedSavedObjects.sort(objectComparator); + response.body.saved_objects.sort(objectComparator); for (let i = 0; i < authorizedSavedObjects.length; i++) { const object = response.body.saved_objects[i]; - const { type: expectedType, id: expectedId } = authorizedSavedObjects[i]; - expect(object.type).to.eql(expectedType); - expect(object.id).to.eql(expectedId); + const expected = authorizedSavedObjects[i]; + const expectedNamespaces = getRedactedNamespaces(user, expected.expectedNamespaces); + expect(object.type).to.eql(expected.type); + expect(object.id).to.eql(expected.id); expect(object.updated_at).to.match(/^[\d-]{10}T[\d:\.]{12}Z$/); - expect(object.namespaces).to.eql(object.namespaces); + expect(object.namespaces).to.eql(expectedNamespaces); // don't test attributes, version, or references } } diff --git a/x-pack/test/saved_object_api_integration/common/suites/get.ts b/x-pack/test/saved_object_api_integration/common/suites/get.ts index fb03cd548d41a..8d8938b5ee79f 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/get.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/get.ts @@ -21,7 +21,7 @@ export type GetTestSuite = TestSuite; export type GetTestCase = TestCase; const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); -export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); +export const TEST_CASES: Record = Object.freeze({ ...CASES, DOES_NOT_EXIST }); export function getTestSuiteFactory(esArchiver: any, supertest: SuperTest) { const expectForbidden = expectResponses.forbiddenTypes('get'); diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index 5036d7b200881..b0d0b4f8a815a 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -36,7 +36,7 @@ const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; // * id: conflict_4a, originId: conflict_4 // using the seven conflict test case objects below, we can exercise various permutations of exact/inexact/ambiguous conflict scenarios const CID = 'conflict_'; -export const TEST_CASES = Object.freeze({ +export const TEST_CASES: Record = Object.freeze({ ...CASES, CONFLICT_1_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}1` }), CONFLICT_1A_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}1a`, originId: `${CID}1` }), diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts index 6d294aed9b4de..02fa614ac2b55 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts @@ -37,7 +37,7 @@ const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; // * id: conflict_3 // * id: conflict_4a, originId: conflict_4 // using the five conflict test case objects below, we can exercise various permutations of exact/inexact/ambiguous conflict scenarios -export const TEST_CASES = Object.freeze({ +export const TEST_CASES: Record = Object.freeze({ ...CASES, CONFLICT_1A_OBJ: Object.freeze({ type: 'sharedtype', diff --git a/x-pack/test/saved_object_api_integration/common/suites/update.ts b/x-pack/test/saved_object_api_integration/common/suites/update.ts index 82f4699babf46..19921a82b2eb4 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/update.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/update.ts @@ -28,7 +28,10 @@ const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, const NEW_ATTRIBUTE_VAL = `Updated attribute value ${Date.now()}`; const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); -export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); +export const TEST_CASES: Record = Object.freeze({ + ...CASES, + DOES_NOT_EXIST, +}); export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest) { const expectForbidden = expectResponses.forbiddenTypes('update'); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts index 0cc5969e2b7ab..93ae439d01166 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts @@ -26,13 +26,23 @@ const unresolvableConflict = (condition?: boolean) => const createTestCases = (overwrite: boolean, spaceId: string) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result + const expectedNamespaces = [spaceId]; // newly created objects should have this `namespaces` array in their return value const normalTypes = [ { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + expectedNamespaces, + }, + { + ...CASES.SINGLE_NAMESPACE_SPACE_1, + ...fail409(!overwrite && spaceId === SPACE_1_ID), + expectedNamespaces, + }, + { + ...CASES.SINGLE_NAMESPACE_SPACE_2, + ...fail409(!overwrite && spaceId === SPACE_2_ID), + expectedNamespaces, }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), @@ -49,8 +59,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { ...unresolvableConflict(spaceId !== SPACE_2_ID), }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - CASES.NEW_MULTI_NAMESPACE_OBJ, + { ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, ]; const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; @@ -68,22 +78,28 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean, spaceId: string) => { + const createTests = (overwrite: boolean, spaceId: string, user: TestUser) => { const { normalTypes, hiddenType, allTypes } = createTestCases(overwrite, spaceId); // use singleRequest to reduce execution time and/or test combined cases return { - unauthorized: createTestDefinitions(allTypes, true, overwrite, { spaceId }), + unauthorized: createTestDefinitions(allTypes, true, overwrite, { spaceId, user }), authorized: [ - createTestDefinitions(normalTypes, false, overwrite, { spaceId, singleRequest: true }), - createTestDefinitions(hiddenType, true, overwrite, { spaceId }), + createTestDefinitions(normalTypes, false, overwrite, { + spaceId, + user, + singleRequest: true, + }), + createTestDefinitions(hiddenType, true, overwrite, { spaceId, user }), createTestDefinitions(allTypes, true, overwrite, { spaceId, + user, singleRequest: true, responseBodyOverride: expectForbidden(['hiddentype']), }), ].flat(), superuser: createTestDefinitions(allTypes, false, overwrite, { spaceId, + user, singleRequest: true, }), }; @@ -93,7 +109,6 @@ export default function ({ getService }: FtrProviderContext) { getTestScenarios([false, true]).securityAndSpaces.forEach( ({ spaceId, users, modifier: overwrite }) => { const suffix = ` within the ${spaceId} space${overwrite ? ' with overwrite enabled' : ''}`; - const { unauthorized, authorized, superuser } = createTests(overwrite!, spaceId); const _addTests = (user: TestUser, tests: BulkCreateTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, spaceId, tests }); }; @@ -106,11 +121,14 @@ export default function ({ getService }: FtrProviderContext) { users.readAtSpace, users.allAtOtherSpace, ].forEach((user) => { + const { unauthorized } = createTests(overwrite!, spaceId, user); _addTests(user, unauthorized); }); [users.dualAll, users.allGlobally, users.allAtSpace].forEach((user) => { + const { authorized } = createTests(overwrite!, spaceId, user); _addTests(user, authorized); }); + const { superuser } = createTests(overwrite!, spaceId, users.superuser); _addTests(users.superuser, superuser); } ); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts index f81488603dc83..7353dafb5e1b5 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts @@ -24,13 +24,23 @@ const { fail400, fail409 } = testCaseFailures; const createTestCases = (overwrite: boolean, spaceId: string) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result + const expectedNamespaces = [spaceId]; // newly created objects should have this `namespaces` array in their return value const normalTypes = [ { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + expectedNamespaces, + }, + { + ...CASES.SINGLE_NAMESPACE_SPACE_1, + ...fail409(!overwrite && spaceId === SPACE_1_ID), + expectedNamespaces, + }, + { + ...CASES.SINGLE_NAMESPACE_SPACE_2, + ...fail409(!overwrite && spaceId === SPACE_2_ID), + expectedNamespaces, }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), @@ -38,8 +48,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - CASES.NEW_MULTI_NAMESPACE_OBJ, + { ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, ]; const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; @@ -53,15 +63,15 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const { addTests, createTestDefinitions } = createTestSuiteFactory(es, esArchiver, supertest); - const createTests = (overwrite: boolean, spaceId: string) => { + const createTests = (overwrite: boolean, spaceId: string, user: TestUser) => { const { normalTypes, hiddenType, allTypes } = createTestCases(overwrite, spaceId); return { - unauthorized: createTestDefinitions(allTypes, true, overwrite, { spaceId }), + unauthorized: createTestDefinitions(allTypes, true, overwrite, { spaceId, user }), authorized: [ - createTestDefinitions(normalTypes, false, overwrite, { spaceId }), - createTestDefinitions(hiddenType, true, overwrite, { spaceId }), + createTestDefinitions(normalTypes, false, overwrite, { spaceId, user }), + createTestDefinitions(hiddenType, true, overwrite, { spaceId, user }), ].flat(), - superuser: createTestDefinitions(allTypes, false, overwrite, { spaceId }), + superuser: createTestDefinitions(allTypes, false, overwrite, { spaceId, user }), }; }; @@ -69,7 +79,6 @@ export default function ({ getService }: FtrProviderContext) { getTestScenarios([false, true]).securityAndSpaces.forEach( ({ spaceId, users, modifier: overwrite }) => { const suffix = ` within the ${spaceId} space${overwrite ? ' with overwrite enabled' : ''}`; - const { unauthorized, authorized, superuser } = createTests(overwrite!, spaceId); const _addTests = (user: TestUser, tests: CreateTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, spaceId, tests }); }; @@ -82,11 +91,14 @@ export default function ({ getService }: FtrProviderContext) { users.readAtSpace, users.allAtOtherSpace, ].forEach((user) => { + const { unauthorized } = createTests(overwrite!, spaceId, user); _addTests(user, unauthorized); }); [users.dualAll, users.allGlobally, users.allAtSpace].forEach((user) => { + const { authorized } = createTests(overwrite!, spaceId, user); _addTests(user, authorized); }); + const { superuser } = createTests(overwrite!, spaceId, users.superuser); _addTests(users.superuser, superuser); } ); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts index c581a1757565e..be3906209032f 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts @@ -15,17 +15,23 @@ import { const createTestCases = (spaceId: string) => { const cases = getTestCases(spaceId); - const exportableTypes = [ + const exportableObjects = [ cases.singleNamespaceObject, - cases.singleNamespaceType, cases.multiNamespaceObject, - cases.multiNamespaceType, cases.namespaceAgnosticObject, + ]; + const exportableTypes = [ + cases.singleNamespaceType, + cases.multiNamespaceType, cases.namespaceAgnosticType, ]; - const nonExportableTypes = [cases.hiddenObject, cases.hiddenType]; - const allTypes = exportableTypes.concat(nonExportableTypes); - return { exportableTypes, nonExportableTypes, allTypes }; + const nonExportableObjectsAndTypes = [cases.hiddenObject, cases.hiddenType]; + const allObjectsAndTypes = [ + exportableObjects, + exportableTypes, + nonExportableObjectsAndTypes, + ].flat(); + return { exportableObjects, exportableTypes, nonExportableObjectsAndTypes, allObjectsAndTypes }; }; export default function ({ getService }: FtrProviderContext) { @@ -34,13 +40,19 @@ export default function ({ getService }: FtrProviderContext) { const { addTests, createTestDefinitions } = exportTestSuiteFactory(esArchiver, supertest); const createTests = (spaceId: string) => { - const { exportableTypes, nonExportableTypes, allTypes } = createTestCases(spaceId); + const { + exportableObjects, + exportableTypes, + nonExportableObjectsAndTypes, + allObjectsAndTypes, + } = createTestCases(spaceId); return { unauthorized: [ - createTestDefinitions(exportableTypes, true), - createTestDefinitions(nonExportableTypes, false), + createTestDefinitions(exportableObjects, { statusCode: 403, reason: 'unauthorized' }), + createTestDefinitions(exportableTypes, { statusCode: 200, reason: 'unauthorized' }), // failure with empty result + createTestDefinitions(nonExportableObjectsAndTypes, false), ].flat(), - authorized: createTestDefinitions(allTypes, false), + authorized: createTestDefinitions(allObjectsAndTypes, false), }; }; diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts index 6ac77507df473..afd4783fab792 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts @@ -4,18 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { SPACES } from '../../common/lib/spaces'; +import { AUTHENTICATION } from '../../common/lib/authentication'; +import { + getTestScenarios, + isUserAuthorizedAtSpace, +} from '../../common/lib/saved_object_test_utils'; import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { findTestSuiteFactory, getTestCases } from '../../common/suites/find'; -const createTestCases = (currentSpace: string, crossSpaceSearch: string[]) => { +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; + +const createTestCases = (currentSpace: string, crossSpaceSearch?: string[]) => { const cases = getTestCases({ currentSpace, crossSpaceSearch }); const normalTypes = [ cases.singleNamespaceType, cases.multiNamespaceType, cases.namespaceAgnosticType, + cases.eachType, cases.pageBeyondTotal, cases.unknownSearchField, cases.filterWithNamespaceAgnosticType, @@ -37,89 +49,72 @@ export default function ({ getService }: FtrProviderContext) { const { addTests, createTestDefinitions } = findTestSuiteFactory(esArchiver, supertest); const createTests = (spaceId: string, user: TestUser) => { - const currentSpaceCases = createTestCases(spaceId, []); + const currentSpaceCases = createTestCases(spaceId); - const explicitCrossSpace = createTestCases(spaceId, ['default', 'space_1', 'space_2']); + const EACH_SPACE = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; + const explicitCrossSpace = createTestCases(spaceId, EACH_SPACE); const wildcardCrossSpace = createTestCases(spaceId, ['*']); - if (user.username === 'elastic') { + if (user.username === AUTHENTICATION.SUPERUSER.username) { return { currentSpace: createTestDefinitions(currentSpaceCases.allTypes, false, { user }), - crossSpace: createTestDefinitions(explicitCrossSpace.allTypes, false, { user }), + crossSpace: [ + createTestDefinitions(explicitCrossSpace.allTypes, false, { user }), + createTestDefinitions(wildcardCrossSpace.allTypes, false, { user }), + ].flat(), }; } - const authorizedAtCurrentSpace = - user.authorizedAtSpaces.includes(spaceId) || user.authorizedAtSpaces.includes('*'); - - const authorizedExplicitCrossSpaces = ['default', 'space_1', 'space_2'].filter( - (s) => - user.authorizedAtSpaces.includes('*') || - (s !== spaceId && user.authorizedAtSpaces.includes(s)) + const isAuthorizedExplicitCrossSpaces = EACH_SPACE.some( + (s) => s !== spaceId && isUserAuthorizedAtSpace(user, s) ); - - const authorizedWildcardCrossSpaces = ['default', 'space_1', 'space_2'].filter( - (s) => user.authorizedAtSpaces.includes('*') || user.authorizedAtSpaces.includes(s) + const isAuthorizedWildcardCrossSpaces = EACH_SPACE.some((s) => + isUserAuthorizedAtSpace(user, s) ); - const explicitCrossSpaceDefinitions = - authorizedExplicitCrossSpaces.length > 0 - ? [ - createTestDefinitions(explicitCrossSpace.normalTypes, false, { user }), - createTestDefinitions( - explicitCrossSpace.hiddenAndUnknownTypes, - { - statusCode: 403, - reason: 'forbidden_types', - }, - { user } - ), - ].flat() - : createTestDefinitions( - explicitCrossSpace.allTypes, - { - statusCode: 403, - reason: 'forbidden_namespaces', - }, + const explicitCrossSpaceDefinitions = isAuthorizedExplicitCrossSpaces + ? [ + createTestDefinitions(explicitCrossSpace.normalTypes, false, { user }), + createTestDefinitions( + explicitCrossSpace.hiddenAndUnknownTypes, + { statusCode: 200, reason: 'unauthorized' }, { user } - ); - - const wildcardCrossSpaceDefinitions = - authorizedWildcardCrossSpaces.length > 0 - ? [ - createTestDefinitions(wildcardCrossSpace.normalTypes, false, { user }), - createTestDefinitions( - wildcardCrossSpace.hiddenAndUnknownTypes, - { - statusCode: 403, - reason: 'forbidden_types', - }, - { user } - ), - ].flat() - : createTestDefinitions( - wildcardCrossSpace.allTypes, - { - statusCode: 403, - reason: 'forbidden_namespaces', - }, + ), + ].flat() + : createTestDefinitions( + explicitCrossSpace.allTypes, + { statusCode: 200, reason: 'unauthorized' }, + { user } + ); + const wildcardCrossSpaceDefinitions = isAuthorizedWildcardCrossSpaces + ? [ + createTestDefinitions(wildcardCrossSpace.normalTypes, false, { user }), + createTestDefinitions( + wildcardCrossSpace.hiddenAndUnknownTypes, + { statusCode: 200, reason: 'unauthorized' }, { user } - ); + ), + ].flat() + : createTestDefinitions( + wildcardCrossSpace.allTypes, + { statusCode: 200, reason: 'unauthorized' }, + { user } + ); return { - currentSpace: authorizedAtCurrentSpace + currentSpace: isUserAuthorizedAtSpace(user, spaceId) ? [ createTestDefinitions(currentSpaceCases.normalTypes, false, { user, }), createTestDefinitions(currentSpaceCases.hiddenAndUnknownTypes, { - statusCode: 403, - reason: 'forbidden_types', + statusCode: 200, + reason: 'unauthorized', }), ].flat() : createTestDefinitions(currentSpaceCases.allTypes, { - statusCode: 403, - reason: 'forbidden_types', + statusCode: 200, + reason: 'unauthorized', }), crossSpace: [...explicitCrossSpaceDefinitions, ...wildcardCrossSpaceDefinitions], }; diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts index 725120687c231..cc2c5e2e7fc00 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SPACES } from '../../common/lib/spaces'; import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -13,22 +14,26 @@ import { BulkCreateTestDefinition, } from '../../common/suites/bulk_create'; +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, +} = SPACES; const { fail400, fail409 } = testCaseFailures; const unresolvableConflict = () => ({ fail409Param: 'unresolvableConflict' }); const createTestCases = (overwrite: boolean) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result + const expectedNamespaces = [DEFAULT_SPACE_ID]; // newly created objects should have this `namespaces` array in their return value const normalTypes = [ { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, - CASES.SINGLE_NAMESPACE_SPACE_1, - CASES.SINGLE_NAMESPACE_SPACE_2, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, expectedNamespaces }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, expectedNamespaces }, { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(), ...unresolvableConflict() }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(), ...unresolvableConflict() }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - CASES.NEW_MULTI_NAMESPACE_OBJ, + { ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, ]; const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; @@ -46,27 +51,27 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean) => { + const createTests = (overwrite: boolean, user: TestUser) => { const { normalTypes, hiddenType, allTypes } = createTestCases(overwrite); // use singleRequest to reduce execution time and/or test combined cases return { - unauthorized: createTestDefinitions(allTypes, true, overwrite), + unauthorized: createTestDefinitions(allTypes, true, overwrite, { user }), authorized: [ - createTestDefinitions(normalTypes, false, overwrite, { singleRequest: true }), - createTestDefinitions(hiddenType, true, overwrite), + createTestDefinitions(normalTypes, false, overwrite, { user, singleRequest: true }), + createTestDefinitions(hiddenType, true, overwrite, { user }), createTestDefinitions(allTypes, true, overwrite, { + user, singleRequest: true, responseBodyOverride: expectForbidden(['hiddentype']), }), ].flat(), - superuser: createTestDefinitions(allTypes, false, overwrite, { singleRequest: true }), + superuser: createTestDefinitions(allTypes, false, overwrite, { user, singleRequest: true }), }; }; describe('_bulk_create', () => { getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { const suffix = overwrite ? ' with overwrite enabled' : ''; - const { unauthorized, authorized, superuser } = createTests(overwrite!); const _addTests = (user: TestUser, tests: BulkCreateTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, tests }); }; @@ -81,11 +86,14 @@ export default function ({ getService }: FtrProviderContext) { users.allAtSpace1, users.readAtSpace1, ].forEach((user) => { + const { unauthorized } = createTests(overwrite!, user); _addTests(user, unauthorized); }); [users.dualAll, users.allGlobally].forEach((user) => { + const { authorized } = createTests(overwrite!, user); _addTests(user, authorized); }); + const { superuser } = createTests(overwrite!, users.superuser); _addTests(users.superuser, superuser); }); }); diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts index 88d096f05d846..b7c6ecef979bd 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SPACES } from '../../common/lib/spaces'; import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -13,21 +14,25 @@ import { CreateTestDefinition, } from '../../common/suites/create'; +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, +} = SPACES; const { fail400, fail409 } = testCaseFailures; const createTestCases = (overwrite: boolean) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result + const expectedNamespaces = [DEFAULT_SPACE_ID]; // newly created objects should have this `namespaces` array in their return value const normalTypes = [ { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, - CASES.SINGLE_NAMESPACE_SPACE_1, - CASES.SINGLE_NAMESPACE_SPACE_2, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, expectedNamespaces }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, expectedNamespaces }, { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409() }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409() }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - CASES.NEW_MULTI_NAMESPACE_OBJ, + { ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, ]; const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; @@ -41,22 +46,21 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const { addTests, createTestDefinitions } = createTestSuiteFactory(es, esArchiver, supertest); - const createTests = (overwrite: boolean) => { + const createTests = (overwrite: boolean, user: TestUser) => { const { normalTypes, hiddenType, allTypes } = createTestCases(overwrite); return { - unauthorized: createTestDefinitions(allTypes, true, overwrite), + unauthorized: createTestDefinitions(allTypes, true, overwrite, { user }), authorized: [ - createTestDefinitions(normalTypes, false, overwrite), - createTestDefinitions(hiddenType, true, overwrite), + createTestDefinitions(normalTypes, false, overwrite, { user }), + createTestDefinitions(hiddenType, true, overwrite, { user }), ].flat(), - superuser: createTestDefinitions(allTypes, false, overwrite), + superuser: createTestDefinitions(allTypes, false, overwrite, { user }), }; }; describe('_create', () => { getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { const suffix = overwrite ? ' with overwrite enabled' : ''; - const { unauthorized, authorized, superuser } = createTests(overwrite!); const _addTests = (user: TestUser, tests: CreateTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, tests }); }; @@ -71,11 +75,14 @@ export default function ({ getService }: FtrProviderContext) { users.allAtSpace1, users.readAtSpace1, ].forEach((user) => { + const { unauthorized } = createTests(overwrite!, user); _addTests(user, unauthorized); }); [users.dualAll, users.allGlobally].forEach((user) => { + const { authorized } = createTests(overwrite!, user); _addTests(user, authorized); }); + const { superuser } = createTests(overwrite!, users.superuser); _addTests(users.superuser, superuser); }); }); diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/export.ts b/x-pack/test/saved_object_api_integration/security_only/apis/export.ts index 99babf683ccfa..ea1ed56921d22 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/export.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/export.ts @@ -15,17 +15,23 @@ import { const createTestCases = () => { const cases = getTestCases(); - const exportableTypes = [ + const exportableObjects = [ cases.singleNamespaceObject, - cases.singleNamespaceType, cases.multiNamespaceObject, - cases.multiNamespaceType, cases.namespaceAgnosticObject, + ]; + const exportableTypes = [ + cases.singleNamespaceType, + cases.multiNamespaceType, cases.namespaceAgnosticType, ]; - const nonExportableTypes = [cases.hiddenObject, cases.hiddenType]; - const allTypes = exportableTypes.concat(nonExportableTypes); - return { exportableTypes, nonExportableTypes, allTypes }; + const nonExportableObjectsAndTypes = [cases.hiddenObject, cases.hiddenType]; + const allObjectsAndTypes = [ + exportableObjects, + exportableTypes, + nonExportableObjectsAndTypes, + ].flat(); + return { exportableObjects, exportableTypes, nonExportableObjectsAndTypes, allObjectsAndTypes }; }; export default function ({ getService }: FtrProviderContext) { @@ -34,13 +40,19 @@ export default function ({ getService }: FtrProviderContext) { const { addTests, createTestDefinitions } = exportTestSuiteFactory(esArchiver, supertest); const createTests = () => { - const { exportableTypes, nonExportableTypes, allTypes } = createTestCases(); + const { + exportableObjects, + exportableTypes, + nonExportableObjectsAndTypes, + allObjectsAndTypes, + } = createTestCases(); return { unauthorized: [ - createTestDefinitions(exportableTypes, true), - createTestDefinitions(nonExportableTypes, false), + createTestDefinitions(exportableObjects, { statusCode: 403, reason: 'unauthorized' }), + createTestDefinitions(exportableTypes, { statusCode: 200, reason: 'unauthorized' }), // failure with empty result + createTestDefinitions(nonExportableObjectsAndTypes, false), ].flat(), - authorized: createTestDefinitions(allTypes, false), + authorized: createTestDefinitions(allObjectsAndTypes, false), }; }; diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/find.ts b/x-pack/test/saved_object_api_integration/security_only/apis/find.ts index 3a435119436ca..aa18f32600949 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/find.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/find.ts @@ -4,18 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SPACES } from '../../common/lib/spaces'; +import { AUTHENTICATION } from '../../common/lib/authentication'; import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { findTestSuiteFactory, getTestCases } from '../../common/suites/find'; -const createTestCases = (crossSpaceSearch: string[]) => { +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; + +const createTestCases = (crossSpaceSearch?: string[]) => { const cases = getTestCases({ crossSpaceSearch }); const normalTypes = [ cases.singleNamespaceType, cases.multiNamespaceType, cases.namespaceAgnosticType, + cases.eachType, cases.pageBeyondTotal, cases.unknownSearchField, cases.filterWithNamespaceAgnosticType, @@ -37,46 +46,35 @@ export default function ({ getService }: FtrProviderContext) { const { addTests, createTestDefinitions } = findTestSuiteFactory(esArchiver, supertest); const createTests = (user: TestUser) => { - const defaultCases = createTestCases([]); - const crossSpaceCases = createTestCases(['default', 'space_1', 'space_2']); + const defaultCases = createTestCases(); + const crossSpaceCases = createTestCases([DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]); - if (user.username === 'elastic') { + if (user.username === AUTHENTICATION.SUPERUSER.username) { return { defaultCases: createTestDefinitions(defaultCases.allTypes, false, { user }), crossSpace: createTestDefinitions( crossSpaceCases.allTypes, - { - statusCode: 400, - reason: 'cross_namespace_not_permitted', - }, + { statusCode: 400, reason: 'cross_namespace_not_permitted' }, { user } ), }; } - const authorizedGlobally = user.authorizedAtSpaces.includes('*'); + const isAuthorizedGlobally = user.authorizedAtSpaces.includes('*'); return { - defaultCases: authorizedGlobally + defaultCases: isAuthorizedGlobally ? [ - createTestDefinitions(defaultCases.normalTypes, false, { - user, - }), + createTestDefinitions(defaultCases.normalTypes, false, { user }), createTestDefinitions(defaultCases.hiddenAndUnknownTypes, { - statusCode: 403, - reason: 'forbidden_types', + statusCode: 200, + reason: 'unauthorized', }), ].flat() - : createTestDefinitions(defaultCases.allTypes, { - statusCode: 403, - reason: 'forbidden_types', - }), + : createTestDefinitions(defaultCases.allTypes, { statusCode: 200, reason: 'unauthorized' }), crossSpace: createTestDefinitions( crossSpaceCases.allTypes, - { - statusCode: 400, - reason: 'cross_namespace_not_permitted', - }, + { statusCode: 400, reason: 'cross_namespace_not_permitted' }, { user } ), }; diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts index 74fade39bf7a5..ef47b09eddbc8 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts @@ -19,36 +19,48 @@ const { fail400, fail409 } = testCaseFailures; const unresolvableConflict = (condition?: boolean) => condition !== false ? { fail409Param: 'unresolvableConflict' } : {}; -const createTestCases = (overwrite: boolean, spaceId: string) => [ +const createTestCases = (overwrite: boolean, spaceId: string) => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result - { - ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, - ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), - }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, - { - ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, - ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), - ...unresolvableConflict(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), - }, - { - ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, - ...fail409(!overwrite || spaceId !== SPACE_1_ID), - ...unresolvableConflict(spaceId !== SPACE_1_ID), - }, - { - ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, - ...fail409(!overwrite || spaceId !== SPACE_2_ID), - ...unresolvableConflict(spaceId !== SPACE_2_ID), - }, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - { ...CASES.HIDDEN, ...fail400() }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - CASES.NEW_MULTI_NAMESPACE_OBJ, - CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, -]; + const expectedNamespaces = [spaceId]; // newly created objects should have this `namespaces` array in their return value + return [ + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + expectedNamespaces, + }, + { + ...CASES.SINGLE_NAMESPACE_SPACE_1, + ...fail409(!overwrite && spaceId === SPACE_1_ID), + expectedNamespaces, + }, + { + ...CASES.SINGLE_NAMESPACE_SPACE_2, + ...fail409(!overwrite && spaceId === SPACE_2_ID), + expectedNamespaces, + }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + ...unresolvableConflict(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, + ...fail409(!overwrite || spaceId !== SPACE_1_ID), + ...unresolvableConflict(spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, + ...fail409(!overwrite || spaceId !== SPACE_2_ID), + ...unresolvableConflict(spaceId !== SPACE_2_ID), + }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + { ...CASES.HIDDEN, ...fail400() }, + { ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; +}; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts index 1040f7fd81dde..10e57b4db82dc 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts @@ -16,27 +16,39 @@ const { } = SPACES; const { fail400, fail409 } = testCaseFailures; -const createTestCases = (overwrite: boolean, spaceId: string) => [ +const createTestCases = (overwrite: boolean, spaceId: string) => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result - { - ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, - ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), - }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, - { - ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, - ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), - }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) }, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - { ...CASES.HIDDEN, ...fail400() }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - CASES.NEW_MULTI_NAMESPACE_OBJ, - CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, -]; + const expectedNamespaces = [spaceId]; // newly created objects should have this `namespaces` array in their return value + return [ + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + expectedNamespaces, + }, + { + ...CASES.SINGLE_NAMESPACE_SPACE_1, + ...fail409(!overwrite && spaceId === SPACE_1_ID), + expectedNamespaces, + }, + { + ...CASES.SINGLE_NAMESPACE_SPACE_2, + ...fail409(!overwrite && spaceId === SPACE_2_ID), + expectedNamespaces, + }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + { ...CASES.HIDDEN, ...fail400() }, + { ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; +}; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts index 1d46985916cd5..c6779402d3291 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts @@ -4,11 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SPACES } from '../../common/lib/spaces'; import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { findTestSuiteFactory, getTestCases } from '../../common/suites/find'; -const createTestCases = (spaceId: string, crossSpaceSearch: string[]) => { +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; + +const createTestCases = (spaceId: string, crossSpaceSearch?: string[]) => { const cases = getTestCases({ currentSpace: spaceId, crossSpaceSearch }); return Object.values(cases); }; @@ -18,15 +25,19 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const { addTests, createTestDefinitions } = findTestSuiteFactory(esArchiver, supertest); - const createTests = (spaceId: string, crossSpaceSearch: string[]) => { + const createTests = (spaceId: string, crossSpaceSearch?: string[]) => { const testCases = createTestCases(spaceId, crossSpaceSearch); return createTestDefinitions(testCases, false); }; describe('_find', () => { getTestScenarios().spaces.forEach(({ spaceId }) => { - const currentSpaceTests = createTests(spaceId, []); - const explicitCrossSpaceTests = createTests(spaceId, ['default', 'space_1', 'space_2']); + const currentSpaceTests = createTests(spaceId); + const explicitCrossSpaceTests = createTests(spaceId, [ + DEFAULT_SPACE_ID, + SPACE_1_ID, + SPACE_2_ID, + ]); const wildcardCrossSpaceTests = createTests(spaceId, ['*']); addTests(`within the ${spaceId} space`, { spaceId, From 0238206ace13c57cd5de58d0d0b14df38d36043b Mon Sep 17 00:00:00 2001 From: Luca Belluccini Date: Tue, 22 Sep 2020 18:44:30 +0200 Subject: [PATCH 10/92] [DOC] Clarify supported realms when accessing remote monitoring clusters (#77938) Co-authored-by: lcawl --- docs/user/monitoring/viewing-metrics.asciidoc | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/user/monitoring/viewing-metrics.asciidoc b/docs/user/monitoring/viewing-metrics.asciidoc index f35caea025cdd..0c48e3b7d011d 100644 --- a/docs/user/monitoring/viewing-metrics.asciidoc +++ b/docs/user/monitoring/viewing-metrics.asciidoc @@ -13,13 +13,19 @@ At a minimum, you must have monitoring data for the {es} production cluster. Once that data exists, {kib} can display monitoring data for other products in the cluster. +TIP: If you use a separate monitoring cluster to store the monitoring data, it +is strongly recommended that you use a separate {kib} instance to view it. If +you log in to {kib} using SAML, Kerberos, PKI, OpenID Connect, or token +authentication providers, a dedicated {kib} instance is *required*. The security +tokens that are used in these contexts are cluster-specific, therefore you +cannot use a single {kib} instance to connect to both production and monitoring +clusters. For more information about the recommended configuration, see +{ref}/monitoring-overview.html[Monitoring overview]. + . Identify where to retrieve monitoring data from. + -- -The cluster that contains the monitoring data is referred to -as the _monitoring cluster_. - -TIP: If the monitoring data is stored on a *dedicated* monitoring cluster, it is +If the monitoring data is stored on a dedicated monitoring cluster, it is accessible even when the cluster you're monitoring is not. If you have at least a gold license, you can send data from multiple clusters to the same monitoring cluster and view them all through the same instance of {kib}. From 311805a57d8109832648db6e3b7fe4143e18e030 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Tue, 22 Sep 2020 12:50:44 -0400 Subject: [PATCH 11/92] [Ingest Manager] Adding bulk packages upgrade api (#77827) * Adding bulk upgrade api * Addressing comments * Removing todo * Changing body field * Adding helper for getting the bulk install route * Adding request spec * Pulling in Johns changes * Removing test for same package upgraded multiple times * Pulling in John's error handling changes * Fixing type error --- .../ingest_manager/common/constants/routes.ts | 2 + .../ingest_manager/common/services/routes.ts | 4 + .../common/types/rest_spec/epm.ts | 24 +++ .../ingest_manager/server/errors/handlers.ts | 29 +-- .../ingest_manager/server/errors/index.ts | 2 +- .../server/routes/epm/handlers.ts | 62 +++--- .../ingest_manager/server/routes/epm/index.ts | 11 + .../server/services/epm/packages/install.ts | 188 +++++++++++++++++- .../server/types/rest_spec/epm.ts | 6 + .../apis/epm/bulk_upgrade.ts | 113 +++++++++++ .../apis/epm/index.js | 1 + 11 files changed, 394 insertions(+), 48 deletions(-) create mode 100644 x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index 3e065142ea101..378a6c6c12159 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -15,9 +15,11 @@ export const LIMITED_CONCURRENCY_ROUTE_TAG = 'ingest:limited-concurrency'; // EPM API routes const EPM_PACKAGES_MANY = `${EPM_API_ROOT}/packages`; +const EPM_PACKAGES_BULK = `${EPM_PACKAGES_MANY}/_bulk`; const EPM_PACKAGES_ONE = `${EPM_PACKAGES_MANY}/{pkgkey}`; const EPM_PACKAGES_FILE = `${EPM_PACKAGES_MANY}/{pkgName}/{pkgVersion}`; export const EPM_API_ROUTES = { + BULK_INSTALL_PATTERN: EPM_PACKAGES_BULK, LIST_PATTERN: EPM_PACKAGES_MANY, LIMITED_LIST_PATTERN: `${EPM_PACKAGES_MANY}/limited`, INFO_PATTERN: EPM_PACKAGES_ONE, diff --git a/x-pack/plugins/ingest_manager/common/services/routes.ts b/x-pack/plugins/ingest_manager/common/services/routes.ts index b7521f95b4f83..ec7c0ee850834 100644 --- a/x-pack/plugins/ingest_manager/common/services/routes.ts +++ b/x-pack/plugins/ingest_manager/common/services/routes.ts @@ -46,6 +46,10 @@ export const epmRouteService = { ); // trim trailing slash }, + getBulkInstallPath: () => { + return EPM_API_ROUTES.BULK_INSTALL_PATTERN; + }, + getRemovePath: (pkgkey: string) => { return EPM_API_ROUTES.DELETE_PATTERN.replace('{pkgkey}', pkgkey).replace(/\/$/, ''); // trim trailing slash }, diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts index 54e767fee4b22..7ed2fed91aa93 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts @@ -71,6 +71,30 @@ export interface InstallPackageResponse { response: AssetReference[]; } +export interface IBulkInstallPackageError { + name: string; + statusCode: number; + error: string | Error; +} + +export interface BulkInstallPackageInfo { + name: string; + newVersion: string; + // this will be null if no package was present before the upgrade (aka it was an install) + oldVersion: string | null; + assets: AssetReference[]; +} + +export interface BulkInstallPackagesResponse { + response: Array; +} + +export interface BulkInstallPackagesRequest { + body: { + packages: string[]; + }; +} + export interface MessageResponse { response: string; } diff --git a/x-pack/plugins/ingest_manager/server/errors/handlers.ts b/x-pack/plugins/ingest_manager/server/errors/handlers.ts index 9f776565cf262..b621f2dd29331 100644 --- a/x-pack/plugins/ingest_manager/server/errors/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/errors/handlers.ts @@ -56,10 +56,7 @@ const getHTTPResponseCode = (error: IngestManagerError): number => { return 400; // Bad Request }; -export const defaultIngestErrorHandler: IngestErrorHandler = async ({ - error, - response, -}: IngestErrorHandlerParams): Promise => { +export function ingestErrorToResponseOptions(error: IngestErrorHandlerParams['error']) { const logger = appContextService.getLogger(); if (isLegacyESClientError(error)) { // there was a problem communicating with ES (e.g. via `callCluster`) @@ -72,36 +69,44 @@ export const defaultIngestErrorHandler: IngestErrorHandler = async ({ logger.error(message); - return response.customError({ + return { statusCode: error?.statusCode || error.status, body: { message }, - }); + }; } // our "expected" errors if (error instanceof IngestManagerError) { // only log the message logger.error(error.message); - return response.customError({ + return { statusCode: getHTTPResponseCode(error), body: { message: error.message }, - }); + }; } // handle any older Boom-based errors or the few places our app uses them if (isBoom(error)) { // only log the message logger.error(error.output.payload.message); - return response.customError({ + return { statusCode: error.output.statusCode, body: { message: error.output.payload.message }, - }); + }; } // not sure what type of error this is. log as much as possible logger.error(error); - return response.customError({ + return { statusCode: 500, body: { message: error.message }, - }); + }; +} + +export const defaultIngestErrorHandler: IngestErrorHandler = async ({ + error, + response, +}: IngestErrorHandlerParams): Promise => { + const options = ingestErrorToResponseOptions(error); + return response.customError(options); }; diff --git a/x-pack/plugins/ingest_manager/server/errors/index.ts b/x-pack/plugins/ingest_manager/server/errors/index.ts index 5e36a2ec9a884..f495bf551dcff 100644 --- a/x-pack/plugins/ingest_manager/server/errors/index.ts +++ b/x-pack/plugins/ingest_manager/server/errors/index.ts @@ -5,7 +5,7 @@ */ /* eslint-disable max-classes-per-file */ -export { defaultIngestErrorHandler } from './handlers'; +export { defaultIngestErrorHandler, ingestErrorToResponseOptions } from './handlers'; export class IngestManagerError extends Error { constructor(message?: string) { diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index c40e0e4ac5c0b..7ae896c1f30a6 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -5,7 +5,6 @@ */ import { TypeOf } from '@kbn/config-schema'; import { RequestHandler, CustomHttpResponseOptions } from 'src/core/server'; -import { appContextService } from '../../services'; import { GetInfoResponse, InstallPackageResponse, @@ -14,6 +13,7 @@ import { GetCategoriesResponse, GetPackagesResponse, GetLimitedPackagesResponse, + BulkInstallPackagesResponse, } from '../../../common'; import { GetCategoriesRequestSchema, @@ -23,6 +23,7 @@ import { InstallPackageFromRegistryRequestSchema, InstallPackageByUploadRequestSchema, DeletePackageRequestSchema, + BulkUpgradePackagesFromRegistryRequestSchema, } from '../../types'; import { getCategories, @@ -34,9 +35,12 @@ import { getLimitedPackages, getInstallationObject, } from '../../services/epm/packages'; -import { IngestManagerError, defaultIngestErrorHandler } from '../../errors'; +import { defaultIngestErrorHandler } from '../../errors'; import { splitPkgKey } from '../../services/epm/registry'; -import { getInstallType } from '../../services/epm/packages/install'; +import { + handleInstallPackageFailure, + bulkInstallPackages, +} from '../../services/epm/packages/install'; export const getCategoriesHandler: RequestHandler< undefined, @@ -136,13 +140,11 @@ export const installPackageFromRegistryHandler: RequestHandler< undefined, TypeOf > = async (context, request, response) => { - const logger = appContextService.getLogger(); const savedObjectsClient = context.core.savedObjects.client; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const { pkgkey } = request.params; const { pkgName, pkgVersion } = splitPkgKey(pkgkey); const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const installType = getInstallType({ pkgVersion, installedPkg }); try { const res = await installPackage({ savedObjectsClient, @@ -155,36 +157,38 @@ export const installPackageFromRegistryHandler: RequestHandler< }; return response.ok({ body }); } catch (e) { - // could have also done `return defaultIngestErrorHandler({ error: e, response })` at each of the returns, - // but doing it this way will log the outer/install errors before any inner/rollback errors const defaultResult = await defaultIngestErrorHandler({ error: e, response }); - if (e instanceof IngestManagerError) { - return defaultResult; - } + await handleInstallPackageFailure({ + savedObjectsClient, + error: e, + pkgName, + pkgVersion, + installedPkg, + callCluster, + }); - // if there is an unknown server error, uninstall any package assets or reinstall the previous version if update - try { - if (installType === 'install' || installType === 'reinstall') { - logger.error(`uninstalling ${pkgkey} after error installing`); - await removeInstallation({ savedObjectsClient, pkgkey, callCluster }); - } - if (installType === 'update') { - // @ts-ignore getInstallType ensures we have installedPkg - const prevVersion = `${pkgName}-${installedPkg.attributes.version}`; - logger.error(`rolling back to ${prevVersion} after error installing ${pkgkey}`); - await installPackage({ - savedObjectsClient, - pkgkey: prevVersion, - callCluster, - }); - } - } catch (error) { - logger.error(`failed to uninstall or rollback package after installation error ${error}`); - } return defaultResult; } }; +export const bulkInstallPackagesFromRegistryHandler: RequestHandler< + undefined, + undefined, + TypeOf +> = async (context, request, response) => { + const savedObjectsClient = context.core.savedObjects.client; + const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; + const res = await bulkInstallPackages({ + savedObjectsClient, + callCluster, + packagesToUpgrade: request.body.packages, + }); + const body: BulkInstallPackagesResponse = { + response: res, + }; + return response.ok({ body }); +}; + export const installPackageByUploadHandler: RequestHandler< undefined, undefined, diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts index 9048652f0e8a9..eaf61335b5e06 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts @@ -14,6 +14,7 @@ import { installPackageFromRegistryHandler, installPackageByUploadHandler, deletePackageHandler, + bulkInstallPackagesFromRegistryHandler, } from './handlers'; import { GetCategoriesRequestSchema, @@ -23,6 +24,7 @@ import { InstallPackageFromRegistryRequestSchema, InstallPackageByUploadRequestSchema, DeletePackageRequestSchema, + BulkUpgradePackagesFromRegistryRequestSchema, } from '../../types'; const MAX_FILE_SIZE_BYTES = 104857600; // 100MB @@ -82,6 +84,15 @@ export const registerRoutes = (router: IRouter) => { installPackageFromRegistryHandler ); + router.post( + { + path: EPM_API_ROUTES.BULK_INSTALL_PATTERN, + validate: BulkUpgradePackagesFromRegistryRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + bulkInstallPackagesFromRegistryHandler + ); + router.post( { path: EPM_API_ROUTES.INSTALL_BY_UPLOAD_PATTERN, diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 54b9c4d3fbb17..800151a41a429 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -6,6 +6,9 @@ import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; import semver from 'semver'; +import Boom from 'boom'; +import { UnwrapPromise } from '@kbn/utility-types'; +import { BulkInstallPackageInfo, IBulkInstallPackageError } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE, MAX_TIME_COMPLETE_INSTALL } from '../../../constants'; import { AssetReference, @@ -32,10 +35,15 @@ import { ArchiveAsset, } from '../kibana/assets/install'; import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; -import { deleteKibanaSavedObjectsAssets } from './remove'; -import { PackageOutdatedError } from '../../../errors'; +import { deleteKibanaSavedObjectsAssets, removeInstallation } from './remove'; +import { + IngestManagerError, + PackageOutdatedError, + ingestErrorToResponseOptions, +} from '../../../errors'; import { getPackageSavedObjects } from './get'; import { installTransformForDataset } from '../elasticsearch/transform/install'; +import { appContextService } from '../../app_context'; export async function installLatestPackage(options: { savedObjectsClient: SavedObjectsClientContract; @@ -94,17 +102,185 @@ export async function ensureInstalledPackage(options: { return installation; } -export async function installPackage({ +export async function handleInstallPackageFailure({ savedObjectsClient, - pkgkey, + error, + pkgName, + pkgVersion, + installedPkg, callCluster, - force = false, }: { + savedObjectsClient: SavedObjectsClientContract; + error: IngestManagerError | Boom | Error; + pkgName: string; + pkgVersion: string; + installedPkg: SavedObject | undefined; + callCluster: CallESAsCurrentUser; +}) { + if (error instanceof IngestManagerError) { + return; + } + const logger = appContextService.getLogger(); + const pkgkey = Registry.pkgToPkgKey({ + name: pkgName, + version: pkgVersion, + }); + + // if there is an unknown server error, uninstall any package assets or reinstall the previous version if update + try { + const installType = getInstallType({ pkgVersion, installedPkg }); + if (installType === 'install' || installType === 'reinstall') { + logger.error(`uninstalling ${pkgkey} after error installing`); + await removeInstallation({ savedObjectsClient, pkgkey, callCluster }); + } + + if (installType === 'update') { + if (!installedPkg) { + logger.error( + `failed to rollback package after installation error ${error} because saved object was undefined` + ); + return; + } + const prevVersion = `${pkgName}-${installedPkg.attributes.version}`; + logger.error(`rolling back to ${prevVersion} after error installing ${pkgkey}`); + await installPackage({ + savedObjectsClient, + pkgkey: prevVersion, + callCluster, + }); + } + } catch (e) { + logger.error(`failed to uninstall or rollback package after installation error ${e}`); + } +} + +type BulkInstallResponse = BulkInstallPackageInfo | IBulkInstallPackageError; +function bulkInstallErrorToOptions({ + pkgToUpgrade, + error, +}: { + pkgToUpgrade: string; + error: Error; +}): IBulkInstallPackageError { + const { statusCode, body } = ingestErrorToResponseOptions(error); + return { + name: pkgToUpgrade, + statusCode, + error: body.message, + }; +} + +interface UpgradePackageParams { + savedObjectsClient: SavedObjectsClientContract; + callCluster: CallESAsCurrentUser; + installedPkg: UnwrapPromise>; + latestPkg: UnwrapPromise>; + pkgToUpgrade: string; +} +async function upgradePackage({ + savedObjectsClient, + callCluster, + installedPkg, + latestPkg, + pkgToUpgrade, +}: UpgradePackageParams): Promise { + if (!installedPkg || semver.gt(latestPkg.version, installedPkg.attributes.version)) { + const pkgkey = Registry.pkgToPkgKey({ + name: latestPkg.name, + version: latestPkg.version, + }); + + try { + const assets = await installPackage({ savedObjectsClient, pkgkey, callCluster }); + return { + name: pkgToUpgrade, + newVersion: latestPkg.version, + oldVersion: installedPkg?.attributes.version ?? null, + assets, + }; + } catch (installFailed) { + await handleInstallPackageFailure({ + savedObjectsClient, + error: installFailed, + pkgName: latestPkg.name, + pkgVersion: latestPkg.version, + installedPkg, + callCluster, + }); + return bulkInstallErrorToOptions({ pkgToUpgrade, error: installFailed }); + } + } else { + // package was already at the latest version + return { + name: pkgToUpgrade, + newVersion: latestPkg.version, + oldVersion: latestPkg.version, + assets: [ + ...installedPkg.attributes.installed_es, + ...installedPkg.attributes.installed_kibana, + ], + }; + } +} + +interface BulkInstallPackagesParams { + savedObjectsClient: SavedObjectsClientContract; + packagesToUpgrade: string[]; + callCluster: CallESAsCurrentUser; +} +export async function bulkInstallPackages({ + savedObjectsClient, + packagesToUpgrade, + callCluster, +}: BulkInstallPackagesParams): Promise { + const installedAndLatestPromises = packagesToUpgrade.map((pkgToUpgrade) => + Promise.all([ + getInstallationObject({ savedObjectsClient, pkgName: pkgToUpgrade }), + Registry.fetchFindLatestPackage(pkgToUpgrade), + ]) + ); + const installedAndLatestResults = await Promise.allSettled(installedAndLatestPromises); + const installResponsePromises = installedAndLatestResults.map(async (result, index) => { + const pkgToUpgrade = packagesToUpgrade[index]; + if (result.status === 'fulfilled') { + const [installedPkg, latestPkg] = result.value; + return upgradePackage({ + savedObjectsClient, + callCluster, + installedPkg, + latestPkg, + pkgToUpgrade, + }); + } else { + return bulkInstallErrorToOptions({ pkgToUpgrade, error: result.reason }); + } + }); + const installResults = await Promise.allSettled(installResponsePromises); + const installResponses = installResults.map((result, index) => { + const pkgToUpgrade = packagesToUpgrade[index]; + if (result.status === 'fulfilled') { + return result.value; + } else { + return bulkInstallErrorToOptions({ pkgToUpgrade, error: result.reason }); + } + }); + + return installResponses; +} + +interface InstallPackageParams { savedObjectsClient: SavedObjectsClientContract; pkgkey: string; callCluster: CallESAsCurrentUser; force?: boolean; -}): Promise { +} + +export async function installPackage({ + savedObjectsClient, + pkgkey, + callCluster, + force = false, +}: InstallPackageParams): Promise { // TODO: change epm API to /packageName/version so we don't need to do this const { pkgName, pkgVersion } = Registry.splitPkgKey(pkgkey); // TODO: calls to getInstallationObject, Registry.fetchInfo, and Registry.fetchFindLatestPackge diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts index d7a801feec34f..5d2a078374854 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts @@ -43,6 +43,12 @@ export const InstallPackageFromRegistryRequestSchema = { ), }; +export const BulkUpgradePackagesFromRegistryRequestSchema = { + body: schema.object({ + packages: schema.arrayOf(schema.string(), { minSize: 1 }), + }), +}; + export const InstallPackageByUploadRequestSchema = { body: schema.buffer(), }; diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts new file mode 100644 index 0000000000000..e377ea5a762f9 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; +import { + BulkInstallPackageInfo, + BulkInstallPackagesResponse, + IBulkInstallPackageError, +} from '../../../../plugins/ingest_manager/common'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + + const deletePackage = async (pkgkey: string) => { + await supertest.delete(`/api/ingest_manager/epm/packages/${pkgkey}`).set('kbn-xsrf', 'xxxx'); + }; + + describe('bulk package upgrade api', async () => { + skipIfNoDockerRegistry(providerContext); + + describe('bulk package upgrade with a package already installed', async () => { + beforeEach(async () => { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + afterEach(async () => { + await deletePackage('multiple_versions-0.1.0'); + await deletePackage('multiple_versions-0.3.0'); + await deletePackage('overrides-0.1.0'); + }); + + it('should return 400 if no packages are requested for upgrade', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/_bulk`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + it('should return 200 and an array for upgrading a package', async function () { + const { body }: { body: BulkInstallPackagesResponse } = await supertest + .post(`/api/ingest_manager/epm/packages/_bulk`) + .set('kbn-xsrf', 'xxxx') + .send({ packages: ['multiple_versions'] }) + .expect(200); + expect(body.response.length).equal(1); + expect(body.response[0].name).equal('multiple_versions'); + const entry = body.response[0] as BulkInstallPackageInfo; + expect(entry.oldVersion).equal('0.1.0'); + expect(entry.newVersion).equal('0.3.0'); + }); + it('should return an error for packages that do not exist', async function () { + const { body }: { body: BulkInstallPackagesResponse } = await supertest + .post(`/api/ingest_manager/epm/packages/_bulk`) + .set('kbn-xsrf', 'xxxx') + .send({ packages: ['multiple_versions', 'blahblah'] }) + .expect(200); + expect(body.response.length).equal(2); + expect(body.response[0].name).equal('multiple_versions'); + const entry = body.response[0] as BulkInstallPackageInfo; + expect(entry.oldVersion).equal('0.1.0'); + expect(entry.newVersion).equal('0.3.0'); + + const err = body.response[1] as IBulkInstallPackageError; + expect(err.statusCode).equal(404); + expect(body.response[1].name).equal('blahblah'); + }); + it('should upgrade multiple packages', async function () { + const { body }: { body: BulkInstallPackagesResponse } = await supertest + .post(`/api/ingest_manager/epm/packages/_bulk`) + .set('kbn-xsrf', 'xxxx') + .send({ packages: ['multiple_versions', 'overrides'] }) + .expect(200); + expect(body.response.length).equal(2); + expect(body.response[0].name).equal('multiple_versions'); + let entry = body.response[0] as BulkInstallPackageInfo; + expect(entry.oldVersion).equal('0.1.0'); + expect(entry.newVersion).equal('0.3.0'); + + entry = body.response[1] as BulkInstallPackageInfo; + expect(entry.oldVersion).equal(null); + expect(entry.newVersion).equal('0.1.0'); + expect(entry.name).equal('overrides'); + }); + }); + + describe('bulk upgrade without package already installed', async () => { + afterEach(async () => { + await deletePackage('multiple_versions-0.3.0'); + }); + + it('should return 200 and an array for upgrading a package', async function () { + const { body }: { body: BulkInstallPackagesResponse } = await supertest + .post(`/api/ingest_manager/epm/packages/_bulk`) + .set('kbn-xsrf', 'xxxx') + .send({ packages: ['multiple_versions'] }) + .expect(200); + expect(body.response.length).equal(1); + expect(body.response[0].name).equal('multiple_versions'); + const entry = body.response[0] as BulkInstallPackageInfo; + expect(entry.oldVersion).equal(null); + expect(entry.newVersion).equal('0.3.0'); + }); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js index 28743ee5f43c2..e509babc9828b 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js @@ -16,6 +16,7 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./install_prerelease')); loadTestFile(require.resolve('./install_remove_assets')); loadTestFile(require.resolve('./install_update')); + loadTestFile(require.resolve('./bulk_upgrade')); loadTestFile(require.resolve('./update_assets')); loadTestFile(require.resolve('./data_stream')); loadTestFile(require.resolve('./package_install_complete')); From e0aeebc149b4ba788364f87a28052c6b0858aeb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 22 Sep 2020 19:32:50 +0200 Subject: [PATCH 12/92] [Logs UI] Correctly filter for log rate anomaly examples with missing dataset (#76775) This fixes #76493 by querying for the "unknown" (i.e. empty) dataset using an exists clause. This should be in line with how ML anomaly detection treats missing partition values. --- .../log_analysis/queries/log_entry_examples.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_examples.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_examples.ts index eac5fa84d85a7..1b6a4c611e177 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_examples.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_examples.ts @@ -33,7 +33,7 @@ export const createLogEntryExamplesQuery = ( }, }, }, - ...(!!dataset + ...(dataset !== '' ? [ { term: { @@ -41,7 +41,19 @@ export const createLogEntryExamplesQuery = ( }, }, ] - : []), + : [ + { + bool: { + must_not: [ + { + exists: { + field: partitionField, + }, + }, + ], + }, + }, + ]), ...(categoryQuery ? [ { From b7cc6d3f2f861303911a9d2913c285d8d883f359 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 22 Sep 2020 10:34:25 -0700 Subject: [PATCH 13/92] [Reporting/PDF] Switch layout to no border (#78036) --- .../server/export_types/printable_pdf/lib/pdf/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js index 1042fd66abad7..8840fd524f3e4 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/index.js @@ -104,7 +104,7 @@ class PdfMaker { table: { body: [[img]], }, - layout: 'simpleBorder', + layout: 'noBorder', }; contents.push(wrappedImg); From f412971d5abd6ed7e885f62d82d1e115ac86e574 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 22 Sep 2020 10:50:39 -0700 Subject: [PATCH 14/92] skip flaky suite (#77969) --- x-pack/test/functional/apps/lens/smokescreen.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 42807a23cb13a..05047fab2517d 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -13,7 +13,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const listingTable = getService('listingTable'); const testSubjects = getService('testSubjects'); - describe('lens smokescreen tests', () => { + // Failing: See https://github.com/elastic/kibana/issues/77969 + describe.skip('lens smokescreen tests', () => { it('should allow creation of lens xy chart', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); From 426df45c6f34e0b7acbe985b5224e2522a9d2074 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Tue, 22 Sep 2020 13:43:35 -0500 Subject: [PATCH 15/92] [ML] Edit text for deleting calendars & add functional tests for Calendars and Filter Lists (#77566) Co-authored-by: Elastic Machine --- .../__snapshots__/calendar_form.test.js.snap | 2 + .../edit/calendar_form/calendar_form.js | 4 +- .../edit/new_event_modal/new_event_modal.js | 17 +- .../settings/calendars/list/calendars_list.js | 22 +- .../add_item_popover.test.js.snap | 12 +- .../add_item_popover/add_item_popover.js | 9 +- .../delete_filter_list_modal.test.js.snap | 1 + .../delete_filter_list_modal.js | 1 + .../edit_description_popover.test.js.snap | 3 + .../edit_description_popover.js | 1 + .../edit_filter_list.test.js.snap | 16 ++ .../edit/__snapshots__/header.test.js.snap | 10 +- .../filter_lists/edit/edit_filter_list.js | 6 +- .../settings/filter_lists/edit/header.js | 3 +- .../settings/filter_lists/list/table.js | 2 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - x-pack/test/functional/apps/ml/index.ts | 1 + .../apps/ml/permissions/full_ml_access.ts | 2 +- .../apps/ml/settings/calendar_creation.ts | 100 +++++++++ .../apps/ml/settings/calendar_delete.ts | 62 ++++++ .../apps/ml/settings/calendar_edit.ts | 101 +++++++++ .../functional/apps/ml/settings/common.ts | 26 +++ .../apps/ml/settings/filter_list_creation.ts | 49 +++++ .../apps/ml/settings/filter_list_delete.ts | 68 ++++++ .../apps/ml/settings/filter_list_edit.ts | 78 +++++++ .../test/functional/apps/ml/settings/index.ts | 20 ++ x-pack/test/functional/services/ml/index.ts | 4 +- .../functional/services/ml/security_ui.ts | 1 - .../services/ml/settings_calendar.ts | 203 +++++++++++++++++- .../services/ml/settings_filter_list.ts | 137 +++++++++++- 31 files changed, 924 insertions(+), 41 deletions(-) create mode 100644 x-pack/test/functional/apps/ml/settings/calendar_creation.ts create mode 100644 x-pack/test/functional/apps/ml/settings/calendar_delete.ts create mode 100644 x-pack/test/functional/apps/ml/settings/calendar_edit.ts create mode 100644 x-pack/test/functional/apps/ml/settings/common.ts create mode 100644 x-pack/test/functional/apps/ml/settings/filter_list_creation.ts create mode 100644 x-pack/test/functional/apps/ml/settings/filter_list_delete.ts create mode 100644 x-pack/test/functional/apps/ml/settings/filter_list_edit.ts create mode 100644 x-pack/test/functional/apps/ml/settings/index.ts diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap index fed435d47dfc6..ad76bb9115617 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap @@ -56,6 +56,7 @@ exports[`CalendarForm Renders calendar form 1`] = ` labelType="label" > - +

{description}

@@ -116,6 +116,7 @@ export const CalendarForm = ({ value={calendarId} onChange={onCalendarIdChange} disabled={isEdit === true || saving === true} + data-test-subj="mlCalendarIdInput" /> @@ -132,6 +133,7 @@ export const CalendarForm = ({ value={description} onChange={onDescriptionChange} disabled={isEdit === true || saving === true} + data-test-subj="mlCalendarDescriptionInput" /> diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js index d80e248674a8f..0b5d2b7b5a3ea 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js @@ -257,7 +257,12 @@ export class NewEventModal extends Component { return ( - + @@ -293,13 +299,18 @@ export class NewEventModal extends Component { - + - + c.calendar_id).join(', '), + }} /> } onCancel={this.closeDestroyModal} @@ -130,18 +135,7 @@ export class CalendarsListUI extends Component { } buttonColor="danger" defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - > -

- c.calendar_id).join(', '), - }} - /> -

-
+ /> ); } diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap b/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap index 6e9cd17deabee..969406724537d 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap @@ -7,7 +7,7 @@ exports[`AddItemPopover calls addItems with multiple items on clicking Add butto button={ @@ -71,6 +72,7 @@ exports[`AddItemPopover calls addItems with multiple items on clicking Add butto grow={false} > @@ -93,7 +95,7 @@ exports[`AddItemPopover opens the popover onButtonClick 1`] = ` button={ @@ -157,6 +160,7 @@ exports[`AddItemPopover opens the popover onButtonClick 1`] = ` grow={false} > @@ -179,7 +183,7 @@ exports[`AddItemPopover renders the popover 1`] = ` button={ @@ -243,6 +248,7 @@ exports[`AddItemPopover renders the popover 1`] = ` grow={false} > diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.js b/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.js index 07e060d87b36a..53a3877e2f1bd 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.js @@ -84,7 +84,7 @@ export class AddItemPopover extends Component { iconSide="right" onClick={this.onButtonClick} isDisabled={this.props.canCreateFilter === false} - data-test-subj="mlFilterListAddItemButton" + data-test-subj="mlFilterListOpenNewItemsPopoverButton" > } > - + @@ -127,6 +131,7 @@ export class AddItemPopover extends Component { } + data-test-subj="mlFilterListDeleteConfirmation" defaultFocusedButton="confirm" onCancel={[Function]} onConfirm={[Function]} diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js index 75fdce8e2bac8..5aafe79645f6a 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js @@ -86,6 +86,7 @@ export class DeleteFilterListModal extends Component { } buttonColor="danger" defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + data-test-subj={'mlFilterListDeleteConfirmation'} /> ); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap b/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap index 9904e90a5afae..268b93923a432 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap @@ -47,6 +47,7 @@ exports[`FilterListUsagePopover opens the popover onButtonClick 1`] = ` labelType="label" > diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js b/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js index 06ace034ca819..b7bcb201f2438 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js @@ -102,6 +102,7 @@ export class EditDescriptionPopover extends Component { name="filter_list_description" value={value} onChange={this.onChange} + data-test-subj={'mlFilterListDescriptionInput'} /> diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap index c2fab64473228..f6a4f76975553 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap @@ -80,6 +80,7 @@ exports[`EditFilterList adds new items to filter list 1`] = ` grow={false} > - +

A test filter list

@@ -180,6 +183,7 @@ exports[`EditFilterListHeader renders the header when creating a new filter list labelType="label" > - +

A test filter list

diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js index 681c54ca9eee0..9ea470a388f02 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js @@ -362,7 +362,10 @@ export class EditFilterListUI extends Component { /> - this.returnToFiltersList()}> + this.returnToFiltersList()} + > updateNewFilterId(e.target.value)} + data-test-subj={'mlNewFilterListIdInput'} /> ); @@ -96,7 +97,7 @@ export const EditFilterListHeader = ({ if (description !== undefined && description.length > 0) { descriptionField = ( - +

{description}

); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.js b/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.js index ed992b4e866ff..9e1457483cb2c 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.js @@ -214,7 +214,7 @@ export function FilterListsTable({ isSelectable={true} data-test-subj="mlFilterListsTable" rowProps={(item) => ({ - 'data-test-subj': `mlFilterListsRow row-${item.filter_id}`, + 'data-test-subj': `mlFilterListRow row-${item.filter_id}`, })} /> diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f626835da8e11..7ca4e02068d41 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10527,8 +10527,6 @@ "xpack.ml.calendarsList.deleteCalendars.deletingCalendarSuccessNotificationMessage": "{messageId} が選択されました", "xpack.ml.calendarsList.deleteCalendarsModal.cancelButtonLabel": "キャンセル", "xpack.ml.calendarsList.deleteCalendarsModal.deleteButtonLabel": "削除", - "xpack.ml.calendarsList.deleteCalendarsModal.deleteCalendarsDescription": "{calendarsCount, plural, one {このカレンダー} other {これらのカレンダー}}を削除しますか?{calendarsList}", - "xpack.ml.calendarsList.deleteCalendarsModal.deleteCalendarTitle": "カレンダーの削除", "xpack.ml.calendarsList.errorWithLoadingListOfCalendarsErrorMessage": "カレンダーのリストの読み込み中にエラーが発生しました。", "xpack.ml.calendarsList.table.allJobsLabel": "すべてのジョブに適用", "xpack.ml.calendarsList.table.deleteButtonLabel": "削除", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d6baa87ca9e2f..2e1fb55777cdf 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10533,8 +10533,6 @@ "xpack.ml.calendarsList.deleteCalendars.deletingCalendarSuccessNotificationMessage": "已删除 {messageId}", "xpack.ml.calendarsList.deleteCalendarsModal.cancelButtonLabel": "取消", "xpack.ml.calendarsList.deleteCalendarsModal.deleteButtonLabel": "删除", - "xpack.ml.calendarsList.deleteCalendarsModal.deleteCalendarsDescription": "是否删除{calendarsCount, plural, one {此日历} other {这些日历}}?{calendarsList}", - "xpack.ml.calendarsList.deleteCalendarsModal.deleteCalendarTitle": "删除日历", "xpack.ml.calendarsList.errorWithLoadingListOfCalendarsErrorMessage": "加载日历列表时出错。", "xpack.ml.calendarsList.table.allJobsLabel": "应用到所有作业", "xpack.ml.calendarsList.table.deleteButtonLabel": "删除", diff --git a/x-pack/test/functional/apps/ml/index.ts b/x-pack/test/functional/apps/ml/index.ts index e224f5c8bb128..74dc0fc3ca9f0 100644 --- a/x-pack/test/functional/apps/ml/index.ts +++ b/x-pack/test/functional/apps/ml/index.ts @@ -46,5 +46,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./anomaly_detection')); loadTestFile(require.resolve('./data_visualizer')); loadTestFile(require.resolve('./data_frame_analytics')); + loadTestFile(require.resolve('./settings')); }); } diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index eed7489b09fe6..c3dde872fa4a6 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -438,7 +438,7 @@ export default function ({ getService }: FtrProviderContext) { 'should display enabled elements of the edit calendar page' ); await ml.settingsFilterList.assertEditDescriptionButtonEnabled(true); - await ml.settingsFilterList.assertAddItemButtonEnabled(true); + await ml.settingsFilterList.assertAddItemsButtonEnabled(true); await ml.testExecution.logTestStep('should display the filter item in the list'); await ml.settingsFilterList.assertFilterItemExists(filterItems[0]); diff --git a/x-pack/test/functional/apps/ml/settings/calendar_creation.ts b/x-pack/test/functional/apps/ml/settings/calendar_creation.ts new file mode 100644 index 0000000000000..5b1e3b0a12b13 --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/calendar_creation.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { asyncForEach, createJobConfig } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + const esArchiver = getService('esArchiver'); + + const calendarId = 'test_calendar_id'; + const jobConfigs = [createJobConfig('test_calendar_ad_1'), createJobConfig('test_calendar_ad_2')]; + + describe('calendar creation', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + + await asyncForEach(jobConfigs, async (jobConfig) => { + await ml.api.createAnomalyDetectionJob(jobConfig); + }); + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + afterEach(async () => { + await ml.api.deleteCalendar(calendarId); + }); + + it('creates new calendar that applies to all jobs', async () => { + await ml.testExecution.logTestStep('calendar creation loads the calendar management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToCalendarManagement(); + + await ml.testExecution.logTestStep('calendar creation loads the new calendar edit page'); + await ml.settingsCalendar.assertCreateCalendarButtonEnabled(true); + await ml.settingsCalendar.navigateToCalendarCreationPage(); + + await ml.testExecution.logTestStep('calendar creation sets calendar to apply to all jobs'); + await ml.settingsCalendar.toggleApplyToAllJobsSwitch(true); + await ml.settingsCalendar.assertJobSelectionNotExists(); + await ml.settingsCalendar.assertJobGroupSelectionNotExists(); + + await ml.testExecution.logTestStep('calendar creation sets the calendar id and description'); + await ml.settingsCalendar.setCalendarId(calendarId); + await ml.settingsCalendar.setCalendarDescription('test calendar description'); + + await ml.testExecution.logTestStep('calendar creation creates new calendar event'); + await ml.settingsCalendar.openNewCalendarEventForm(); + await ml.settingsCalendar.setCalendarEventDescription('holiday'); + await ml.settingsCalendar.addNewCalendarEvent(); + await ml.settingsCalendar.assertEventRowExists('holiday'); + + await ml.testExecution.logTestStep( + 'calendar creation saves the new calendar and displays it in the list of calendars ' + ); + await ml.settingsCalendar.saveCalendar(); + + await ml.settingsCalendar.assertCalendarRowExists(calendarId); + }); + + it('creates new calendar that applies to specific jobs', async () => { + await ml.testExecution.logTestStep('calendar creation loads the calendar management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToCalendarManagement(); + + await ml.testExecution.logTestStep('calendar creation loads the new calendar edit page'); + await ml.settingsCalendar.assertCreateCalendarButtonEnabled(true); + await ml.settingsCalendar.navigateToCalendarCreationPage(); + + await ml.testExecution.logTestStep( + 'calendar creation verifies the job selection and job group section are displayed' + ); + await ml.settingsCalendar.assertJobSelectionExists(); + await ml.settingsCalendar.assertJobSelectionEnabled(true); + await ml.settingsCalendar.assertJobGroupSelectionExists(); + await ml.settingsCalendar.assertJobGroupSelectionEnabled(true); + + await ml.testExecution.logTestStep('calendar creation sets the calendar id'); + await ml.settingsCalendar.setCalendarId(calendarId); + + await ml.testExecution.logTestStep('calendar creation sets the job selection'); + await asyncForEach(jobConfigs, async (jobConfig) => { + await ml.settingsCalendar.selectJob(jobConfig.job_id); + }); + + await ml.settingsCalendar.saveCalendar(); + await ml.settingsCalendar.assertCalendarRowExists(calendarId); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/calendar_delete.ts b/x-pack/test/functional/apps/ml/settings/calendar_delete.ts new file mode 100644 index 0000000000000..2cc4f91d5528f --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/calendar_delete.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { asyncForEach } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + + const testDataList = [1, 2].map((n) => ({ + calendarId: `test_delete_calendar_${n}`, + description: `test description ${n}`, + })); + + describe('calendar delete', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + + await asyncForEach(testDataList, async ({ calendarId, description }) => { + await ml.api.createCalendar(calendarId, { + description, + }); + }); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + + // clean up created calendars + await asyncForEach(testDataList, async ({ calendarId }) => { + await ml.api.deleteCalendar(calendarId); + }); + }); + + it('deletes multiple calendars', async () => { + await ml.testExecution.logTestStep('calendar delete loads the calendar list management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToCalendarManagement(); + + await ml.testExecution.logTestStep('calendar delete selects multiple calendars for deletion'); + await asyncForEach(testDataList, async ({ calendarId }) => { + await ml.settingsCalendar.assertCalendarRowExists(calendarId); + await ml.settingsCalendar.selectCalendarRow(calendarId); + }); + + await ml.testExecution.logTestStep('calendar delete clicks the delete button'); + await ml.settingsCalendar.deleteCalendar(); + + await ml.testExecution.logTestStep( + 'calendar delete validates the calendars are deleted from the table' + ); + await asyncForEach(testDataList, async ({ calendarId }) => { + await ml.settingsCalendar.assertCalendarRowNotExists(calendarId); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/calendar_edit.ts b/x-pack/test/functional/apps/ml/settings/calendar_edit.ts new file mode 100644 index 0000000000000..f7c8c1f6f85f5 --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/calendar_edit.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { asyncForEach, createJobConfig } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + const esArchiver = getService('esArchiver'); + const comboBox = getService('comboBox'); + + const calendarId = 'test_edit_calendar_id'; + const testEvents = [ + { description: 'event_1', start_time: 1513641600000, end_time: 1513728000000 }, + { description: 'event_2', start_time: 1513814400000, end_time: 1513900800000 }, + ]; + const jobConfigs = [createJobConfig('test_calendar_ad_1'), createJobConfig('test_calendar_ad_2')]; + const newJobGroups = ['farequote']; + + describe('calendar edit', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + + await asyncForEach(jobConfigs, async (jobConfig) => { + await ml.api.createAnomalyDetectionJob(jobConfig); + }); + + await ml.api.createCalendar(calendarId, { + job_ids: jobConfigs.map((c) => c.job_id), + description: 'Test calendar', + }); + await ml.api.createCalendarEvents(calendarId, testEvents); + + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + afterEach(async () => { + await ml.api.deleteCalendar(calendarId); + }); + + it('updates jobs, groups and events', async () => { + await ml.testExecution.logTestStep('calendar edit loads the calendar management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToCalendarManagement(); + + await ml.testExecution.logTestStep('calendar edit opens existing calendar'); + await ml.settingsCalendar.openCalendarEditForm(calendarId); + + await ml.testExecution.logTestStep( + 'calendar edit deselects previous job selection and assigns new job groups' + ); + await comboBox.clear('mlCalendarJobSelection'); + await asyncForEach(newJobGroups, async (newJobGroup) => { + await ml.settingsCalendar.selectJobGroup(newJobGroup); + }); + + await ml.testExecution.logTestStep('calendar edit deletes old events'); + + await asyncForEach(testEvents, async ({ description }) => { + await ml.settingsCalendar.deleteCalendarEventRow(description); + }); + + await ml.testExecution.logTestStep('calendar edit creates new calendar event'); + await ml.settingsCalendar.openNewCalendarEventForm(); + await ml.settingsCalendar.setCalendarEventDescription('holiday'); + await ml.settingsCalendar.addNewCalendarEvent(); + await ml.settingsCalendar.assertEventRowExists('holiday'); + + await ml.testExecution.logTestStep( + 'calendar edit saves the new calendar and displays it in the list of calendars ' + ); + await ml.settingsCalendar.saveCalendar(); + await ml.settingsCalendar.assertCalendarRowExists(calendarId); + + await ml.testExecution.logTestStep('calendar edit re-opens the updated calendar'); + await ml.settingsCalendar.openCalendarEditForm(calendarId); + await ml.testExecution.logTestStep('calendar edit verifies the job selection is empty'); + await ml.settingsCalendar.assertJobSelection([]); + await ml.testExecution.logTestStep( + 'calendar edit verifies the job group selection was updated' + ); + await ml.settingsCalendar.assertJobGroupSelection(newJobGroups); + + await ml.testExecution.logTestStep('calendar edit verifies calendar updated correctly'); + await asyncForEach(testEvents, async ({ description }) => { + await ml.settingsCalendar.assertEventRowMissing(description); + }); + await ml.settingsCalendar.assertEventRowExists('holiday'); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/common.ts b/x-pack/test/functional/apps/ml/settings/common.ts new file mode 100644 index 0000000000000..9fada028ff3da --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/common.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export async function asyncForEach(array: T[], callback: (item: T, index: number) => void) { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index); + } +} + +export const createJobConfig = (jobId: string) => ({ + job_id: jobId, + description: + 'mean/min/max(responsetime) partition=airline on farequote dataset with 1h bucket span', + groups: ['farequote', 'automated', 'multi-metric'], + analysis_config: { + bucket_span: '1h', + influencers: ['airline'], + detectors: [{ function: 'mean', field_name: 'responsetime', partition_field_name: 'airline' }], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '20mb' }, + model_plot_config: { enabled: false }, +}); diff --git a/x-pack/test/functional/apps/ml/settings/filter_list_creation.ts b/x-pack/test/functional/apps/ml/settings/filter_list_creation.ts new file mode 100644 index 0000000000000..22affa1cada38 --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/filter_list_creation.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + const filterId = 'test_create_filter'; + const description = 'test description'; + const keywords = ['filter word 1', 'filter word 2', 'filter word 3']; + + describe('filter list creation', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + // clean up created filters + await ml.api.deleteFilter(filterId); + }); + + it('creates new filter list', async () => { + await ml.testExecution.logTestStep( + 'filter list creation loads the filter list management page' + ); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToFilterListsManagement(); + + await ml.testExecution.logTestStep('filter list creation loads the filter creation page'); + await ml.settingsFilterList.navigateToFilterListCreationPage(); + + await ml.testExecution.logTestStep('filter list creation sets the list name and description'); + await ml.settingsFilterList.setFilterListId(filterId); + await ml.settingsFilterList.setFilterListDescription(description); + + await ml.testExecution.logTestStep('filter list creation adds items to the filter list'); + await ml.settingsFilterList.addFilterListKeywords(keywords); + await ml.testExecution.logTestStep('filter list creation saves the settings'); + await ml.settingsFilterList.saveFilterList(); + await ml.settingsFilterList.assertFilterListRowExists(filterId); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/filter_list_delete.ts b/x-pack/test/functional/apps/ml/settings/filter_list_delete.ts new file mode 100644 index 0000000000000..9e30d2c8915d2 --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/filter_list_delete.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { asyncForEach } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + + const testDataList = [1, 2].map((n) => ({ + filterId: `test_delete_filter_${n}`, + description: `test description ${n}`, + items: ['filter word 1', 'filter word 2', 'filter word 3'], + })); + + describe('filter list delete', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + + for (let index = 0; index < testDataList.length; index++) { + const { filterId, description, items } = testDataList[index]; + + await ml.api.createFilter(filterId, { + description, + items, + }); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + + // clean up created filters + await asyncForEach(testDataList, async ({ filterId }) => { + await ml.api.deleteFilter(filterId); + }); + }); + + it('deletes filter list with items', async () => { + await ml.testExecution.logTestStep( + 'filter list delete loads the filter list management page' + ); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToFilterListsManagement(); + + await ml.testExecution.logTestStep( + 'filter list delete selects list entries and deletes them' + ); + for (const testData of testDataList) { + const { filterId } = testData; + await ml.settingsFilterList.selectFilterListRow(filterId); + } + await ml.settingsFilterList.deleteFilterList(); + + await ml.testExecution.logTestStep( + 'filter list delete validates selected filter lists are deleted' + ); + await asyncForEach(testDataList, async ({ filterId }) => { + await ml.settingsFilterList.assertFilterListRowNotExists(filterId); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/filter_list_edit.ts b/x-pack/test/functional/apps/ml/settings/filter_list_edit.ts new file mode 100644 index 0000000000000..8c39c679ac6f2 --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/filter_list_edit.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { asyncForEach } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + + const filterId = 'test_filter_list_edit'; + const keywordToDelete = 'keyword_to_delete'; + const oldKeyword = 'old_keyword'; + const oldDescription = 'Old filter list description'; + + const newKeywords = ['new_keyword1', 'new_keyword2']; + const newDescription = 'New filter list description'; + + describe('filter list edit', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + + await ml.api.createFilter(filterId, { + description: oldDescription, + items: [keywordToDelete, oldKeyword], + }); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + await ml.api.deleteFilter(filterId); + }); + + it('updates description and filter items', async () => { + await ml.testExecution.logTestStep('filter list edit loads the filter list management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToFilterListsManagement(); + + await ml.testExecution.logTestStep('filter list edit opens existing filter list'); + await ml.settingsFilterList.selectFilterListRowEditLink(filterId); + await ml.settingsFilterList.assertFilterItemExists(keywordToDelete); + await ml.settingsFilterList.assertFilterListDescriptionValue(oldDescription); + + await ml.testExecution.logTestStep('filter list edit deletes existing filter item'); + await ml.settingsFilterList.deleteFilterItem(keywordToDelete); + + await ml.testExecution.logTestStep('filter list edit sets new keywords and description'); + await ml.settingsFilterList.setFilterListDescription(newDescription); + await ml.settingsFilterList.addFilterListKeywords(newKeywords); + + await ml.testExecution.logTestStep( + 'filter list edit saves the new filter list and displays it in the list of entries' + ); + await ml.settingsFilterList.saveFilterList(); + await ml.settingsFilterList.assertFilterListRowExists(filterId); + + await ml.testExecution.logTestStep('filter list edit reopens the edited filter list'); + await ml.settingsFilterList.selectFilterListRowEditLink(filterId); + + await ml.testExecution.logTestStep( + 'filter list edit verifies the filter list description updated correctly' + ); + await ml.settingsFilterList.assertFilterListDescriptionValue(newDescription); + + await ml.testExecution.logTestStep( + 'filter list edit verifies the filter items updated correctly' + ); + await ml.settingsFilterList.assertFilterItemNotExists(keywordToDelete); + await asyncForEach([...newKeywords, oldKeyword], async (filterItem) => { + await ml.settingsFilterList.assertFilterItemExists(filterItem); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/index.ts b/x-pack/test/functional/apps/ml/settings/index.ts new file mode 100644 index 0000000000000..5b2c7d15e1959 --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('settings', function () { + this.tags(['quynh', 'skipFirefox']); + + loadTestFile(require.resolve('./calendar_creation')); + loadTestFile(require.resolve('./calendar_edit')); + loadTestFile(require.resolve('./calendar_delete')); + + loadTestFile(require.resolve('./filter_list_creation')); + loadTestFile(require.resolve('./filter_list_edit')); + loadTestFile(require.resolve('./filter_list_delete')); + }); +} diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index 325ea41ae3977..50da8425e493d 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -78,8 +78,8 @@ export function MachineLearningProvider(context: FtrProviderContext) { const securityCommon = MachineLearningSecurityCommonProvider(context); const securityUI = MachineLearningSecurityUIProvider(context, securityCommon); const settings = MachineLearningSettingsProvider(context); - const settingsCalendar = MachineLearningSettingsCalendarProvider(context); - const settingsFilterList = MachineLearningSettingsFilterListProvider(context); + const settingsCalendar = MachineLearningSettingsCalendarProvider(context, commonUI); + const settingsFilterList = MachineLearningSettingsFilterListProvider(context, commonUI); const singleMetricViewer = MachineLearningSingleMetricViewerProvider(context); const testExecution = MachineLearningTestExecutionProvider(context); const testResources = MachineLearningTestResourcesProvider(context); diff --git a/x-pack/test/functional/services/ml/security_ui.ts b/x-pack/test/functional/services/ml/security_ui.ts index e09467ff36a34..da4324901d38e 100644 --- a/x-pack/test/functional/services/ml/security_ui.ts +++ b/x-pack/test/functional/services/ml/security_ui.ts @@ -16,7 +16,6 @@ export function MachineLearningSecurityUIProvider( return { async loginAs(user: USER) { const password = mlSecurityCommon.getPasswordForUser(user); - await PageObjects.security.forceLogout(); await PageObjects.security.login(user, password, { diff --git a/x-pack/test/functional/services/ml/settings_calendar.ts b/x-pack/test/functional/services/ml/settings_calendar.ts index 34d18c6e12c47..c269636522923 100644 --- a/x-pack/test/functional/services/ml/settings_calendar.ts +++ b/x-pack/test/functional/services/ml/settings_calendar.ts @@ -7,9 +7,15 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { MlCommonUI } from './common_ui'; -export function MachineLearningSettingsCalendarProvider({ getService }: FtrProviderContext) { +export function MachineLearningSettingsCalendarProvider( + { getService }: FtrProviderContext, + mlCommonUI: MlCommonUI +) { const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const comboBox = getService('comboBox'); return { async parseCalendarTable() { @@ -172,6 +178,11 @@ export function MachineLearningSettingsCalendarProvider({ getService }: FtrProvi ); }, + calendarRowSelector(calendarId: string, subSelector?: string) { + const row = `~mlCalendarTable > ~row-${calendarId}`; + return !subSelector ? row : `${row} > ${subSelector}`; + }, + eventRowSelector(eventDescription: string, subSelector?: string) { const row = `~mlCalendarEventsTable > ~row-${eventDescription}`; return !subSelector ? row : `${row} > ${subSelector}`; @@ -181,6 +192,10 @@ export function MachineLearningSettingsCalendarProvider({ getService }: FtrProvi await testSubjects.existOrFail(this.eventRowSelector(eventDescription)); }, + async assertEventRowMissing(eventDescription: string) { + await testSubjects.missingOrFail(this.eventRowSelector(eventDescription)); + }, + async assertDeleteEventButtonEnabled(eventDescription: string, expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled( this.eventRowSelector(eventDescription, 'mlCalendarEventDeleteButton') @@ -192,5 +207,191 @@ export function MachineLearningSettingsCalendarProvider({ getService }: FtrProvi }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); }, + + async assertCalendarRowExists(calendarId: string) { + await testSubjects.existOrFail(this.calendarRowSelector(calendarId)); + }, + + async assertCalendarRowNotExists(calendarId: string) { + await testSubjects.missingOrFail(this.calendarRowSelector(calendarId)); + }, + + async assertCalendarIdValue(expectedValue: string) { + const actualValue = await testSubjects.getAttribute('mlCalendarIdInput', 'value'); + expect(actualValue).to.eql( + expectedValue, + `Calendar id should be '${expectedValue}' (got '${actualValue}')` + ); + }, + + async setCalendarId(calendarId: string) { + await mlCommonUI.setValueWithChecks('mlCalendarIdInput', calendarId, { + clearWithKeyboard: true, + }); + await this.assertCalendarIdValue(calendarId); + }, + + async assertCalendarDescriptionValue(expectedValue: string) { + const actualValue = await testSubjects.getAttribute('mlCalendarDescriptionInput', 'value'); + expect(actualValue).to.eql( + expectedValue, + `Calendar description should be '${expectedValue}' (got '${actualValue}')` + ); + }, + + async setCalendarDescription(description: string) { + await mlCommonUI.setValueWithChecks('mlCalendarDescriptionInput', description, { + clearWithKeyboard: true, + }); + await this.assertCalendarDescriptionValue(description); + }, + + async getApplyToAllJobsSwitchCheckedState(): Promise { + const subj = 'mlCalendarApplyToAllJobsSwitch'; + const isSelected = await testSubjects.getAttribute(subj, 'aria-checked'); + return isSelected === 'true'; + }, + + async toggleApplyToAllJobsSwitch(toggle: boolean) { + const subj = 'mlCalendarApplyToAllJobsSwitch'; + if ((await this.getApplyToAllJobsSwitchCheckedState()) !== toggle) { + await retry.tryForTime(5 * 1000, async () => { + await testSubjects.clickWhenNotDisabled(subj); + await this.assertApplyToAllJobsSwitchEnabled(toggle); + }); + } + }, + + async saveCalendar() { + await testSubjects.existOrFail('mlSaveCalendarButton'); + await testSubjects.click('mlSaveCalendarButton'); + await testSubjects.existOrFail('mlPageCalendarManagement'); + }, + + async navigateToCalendarCreationPage() { + await testSubjects.existOrFail('mlCalendarButtonCreate'); + await testSubjects.click('mlCalendarButtonCreate'); + await testSubjects.existOrFail('mlPageCalendarEdit'); + }, + + async openNewCalendarEventForm() { + await testSubjects.existOrFail('mlCalendarNewEventButton'); + await testSubjects.click('mlCalendarNewEventButton'); + await testSubjects.existOrFail('mlPageCalendarEdit'); + }, + + async assertCalendarEventDescriptionValue(expectedValue: string) { + const actualValue = await testSubjects.getAttribute( + 'mlCalendarEventDescriptionInput', + 'value' + ); + expect(actualValue).to.eql( + expectedValue, + `Calendar event description should be '${expectedValue}' (got '${actualValue}')` + ); + }, + + async setCalendarEventDescription(eventDescription: string) { + await testSubjects.existOrFail('mlCalendarEventDescriptionInput'); + await mlCommonUI.setValueWithChecks('mlCalendarEventDescriptionInput', eventDescription, { + clearWithKeyboard: true, + }); + await this.assertCalendarEventDescriptionValue(eventDescription); + }, + + async cancelNewCalendarEvent() { + await testSubjects.existOrFail('mlCalendarCancelEventButton'); + await testSubjects.click('mlCalendarCancelEventButton'); + await testSubjects.missingOrFail('mlCalendarEventForm'); + }, + + async addNewCalendarEvent() { + await testSubjects.existOrFail('mlCalendarAddEventButton'); + await testSubjects.click('mlCalendarAddEventButton'); + await testSubjects.missingOrFail('mlCalendarEventForm'); + }, + + async assertJobSelectionExists() { + await testSubjects.existOrFail('mlCalendarJobSelection'); + }, + + async assertJobSelectionNotExists() { + await testSubjects.missingOrFail('mlCalendarJobSelection'); + }, + + async assertJobSelection(expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlCalendarJobSelection > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected job selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }, + + async assertJobSelectionContain(expectedIdentifier: string) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlCalendarJobSelection > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.contain( + expectedIdentifier, + `Expected job selection to contain '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }, + + async selectJob(identifier: string) { + await comboBox.set('mlCalendarJobSelection > comboBoxInput', identifier); + await this.assertJobSelectionContain(identifier); + }, + + async assertJobGroupSelectionExists() { + await testSubjects.existOrFail('mlCalendarJobGroupSelection'); + }, + + async assertJobGroupSelectionNotExists() { + await testSubjects.missingOrFail('mlCalendarJobGroupSelection'); + }, + + async assertJobGroupSelection(expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlCalendarJobGroupSelection > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected job group selection to be'${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }, + + async assertJobGroupSelectionContain(expectedIdentifier: string) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlCalendarJobGroupSelection > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.contain( + expectedIdentifier, + `Expected job group selection to contain'${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }, + + async selectJobGroup(identifier: string) { + await comboBox.set('mlCalendarJobGroupSelection > comboBoxInput', identifier); + await this.assertJobGroupSelectionContain(identifier); + }, + + async deleteCalendarEventRow(eventDescription: string) { + await this.assertEventRowExists(eventDescription); + await testSubjects.click( + this.eventRowSelector(eventDescription, 'mlCalendarEventDeleteButton') + ); + await this.assertEventRowMissing(eventDescription); + }, + + async deleteCalendar() { + await this.assertDeleteCalendarButtonEnabled(true); + await testSubjects.click('mlCalendarButtonDelete'); + await testSubjects.existOrFail('mlCalendarDeleteConfirmation'); + await testSubjects.existOrFail('confirmModalConfirmButton'); + await testSubjects.click('confirmModalConfirmButton'); + await testSubjects.missingOrFail('mlCalendarDeleteConfirmation'); + }, }; } diff --git a/x-pack/test/functional/services/ml/settings_filter_list.ts b/x-pack/test/functional/services/ml/settings_filter_list.ts index 0afe9f21b03a6..bcac575b65c08 100644 --- a/x-pack/test/functional/services/ml/settings_filter_list.ts +++ b/x-pack/test/functional/services/ml/settings_filter_list.ts @@ -7,9 +7,14 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { MlCommonUI } from './common_ui'; -export function MachineLearningSettingsFilterListProvider({ getService }: FtrProviderContext) { +export function MachineLearningSettingsFilterListProvider( + { getService }: FtrProviderContext, + mlCommonUI: MlCommonUI +) { const testSubjects = getService('testSubjects'); + const browser = getService('browser'); return { async parseFilterListTable() { @@ -17,7 +22,7 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro const $ = await table.parseDomContent(); const rows = []; - for (const tr of $.findTestSubjects('~mlFilterListsRow').toArray()) { + for (const tr of $.findTestSubjects('~mlFilterListRow').toArray()) { const $tr = $(tr); const inUseSubject = $tr @@ -55,6 +60,14 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro return !subSelector ? row : `${row} > ${subSelector}`; }, + async assertFilterListRowExists(filterId: string) { + return await testSubjects.existOrFail(this.rowSelector(filterId)); + }, + + async assertFilterListRowNotExists(filterId: string) { + return await testSubjects.missingOrFail(this.rowSelector(filterId)); + }, + async filterWithSearchString(filter: string, expectedRowCount: number = 1) { const tableListContainer = await testSubjects.find('mlFilterListTableContainer'); const searchBarInput = await tableListContainer.findByClassName('euiFieldSearch'); @@ -101,6 +114,12 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro await this.assertFilterListRowSelected(filterId, false); }, + async selectFilterListRowEditLink(filterId: string) { + await this.assertFilterListRowExists(filterId); + await testSubjects.click(this.rowSelector(filterId, `mlEditFilterListLink`)); + await testSubjects.existOrFail('mlPageFilterListEdit'); + }, + async assertCreateFilterListButtonEnabled(expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled('mlFilterListsButtonCreate'); expect(isEnabled).to.eql( @@ -111,6 +130,10 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro ); }, + async assertDeleteFilterListButtonExists() { + await testSubjects.existOrFail('mlFilterListsDeleteButton'); + }, + async assertDeleteFilterListButtonEnabled(expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled('mlFilterListsDeleteButton'); expect(isEnabled).to.eql( @@ -121,6 +144,16 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro ); }, + async deleteFilterList() { + await this.assertDeleteFilterListButtonExists(); + await this.assertDeleteFilterListButtonEnabled(true); + await testSubjects.click('mlFilterListsDeleteButton'); + await testSubjects.existOrFail('mlFilterListsDeleteButton'); + await testSubjects.existOrFail('mlFilterListDeleteConfirmation'); + await testSubjects.click('confirmModalConfirmButton'); + await testSubjects.missingOrFail('mlFilterListDeleteConfirmation'); + }, + async openFilterListEditForm(filterId: string) { await testSubjects.click(this.rowSelector(filterId, 'mlEditFilterListLink')); await testSubjects.existOrFail('mlPageFilterListEdit'); @@ -136,8 +169,8 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro ); }, - async assertAddItemButtonEnabled(expectedValue: boolean) { - const isEnabled = await testSubjects.isEnabled('mlFilterListAddItemButton'); + async assertOpenNewItemsPopoverButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListOpenNewItemsPopoverButton'); expect(isEnabled).to.eql( expectedValue, `Expected "add item" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ @@ -146,6 +179,16 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro ); }, + async assertAddItemsButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListOpenNewItemsPopoverButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "add" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + async assertDeleteItemButtonEnabled(expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled('mlFilterListDeleteItemButton'); expect(isEnabled).to.eql( @@ -156,11 +199,25 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro ); }, + async assertSaveFilterListButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListSaveButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "save filter list" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + filterItemSelector(filterItem: string, subSelector?: string) { const row = `mlGridItem ${filterItem}`; return !subSelector ? row : `${row} > ${subSelector}`; }, + async assertFilterItemNotExists(filterItem: string) { + await testSubjects.missingOrFail(this.filterItemSelector(filterItem)); + }, + async assertFilterItemExists(filterItem: string) { await testSubjects.existOrFail(this.filterItemSelector(filterItem)); }, @@ -189,6 +246,13 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro await this.assertFilterItemSelected(filterItem, true); }, + async deleteFilterItem(filterItem: string) { + await testSubjects.existOrFail('mlFilterListDeleteItemButton'); + await this.selectFilterItem(filterItem); + await testSubjects.click('mlFilterListDeleteItemButton'); + await this.assertFilterItemNotExists(filterItem); + }, + async deselectFilterItem(filterItem: string) { if ((await this.isFilterItemSelected(filterItem)) === true) { await testSubjects.click(this.filterItemSelector(filterItem)); @@ -196,5 +260,70 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro await this.assertFilterItemSelected(filterItem, false); }, + + async navigateToFilterListCreationPage() { + await this.assertCreateFilterListButtonEnabled(true); + await testSubjects.click('mlFilterListsButtonCreate'); + await testSubjects.existOrFail('mlPageFilterListEdit'); + }, + + async assertFilterListIdValue(expectedValue: string) { + const subj = 'mlNewFilterListIdInput'; + const actualFilterListId = await testSubjects.getAttribute(subj, 'value'); + expect(actualFilterListId).to.eql( + expectedValue, + `Filter list id should be '${expectedValue}' (got '${actualFilterListId}')` + ); + }, + + async setFilterListId(filterId: string) { + const subj = 'mlNewFilterListIdInput'; + await mlCommonUI.setValueWithChecks(subj, filterId, { + clearWithKeyboard: true, + }); + await this.assertFilterListIdValue(filterId); + }, + + async setFilterListDescription(description: string) { + await this.assertEditDescriptionButtonEnabled(true); + await testSubjects.click('mlFilterListEditDescriptionButton'); + await testSubjects.existOrFail('mlFilterListDescriptionInput'); + await mlCommonUI.setValueWithChecks('mlFilterListDescriptionInput', description, { + clearWithKeyboard: true, + }); + await browser.pressKeys(browser.keys.ESCAPE); + await this.assertFilterListDescriptionValue(description); + }, + + async addFilterListKeywords(keywords: string[]) { + await this.assertOpenNewItemsPopoverButtonEnabled(true); + await testSubjects.click('mlFilterListOpenNewItemsPopoverButton'); + await mlCommonUI.setValueWithChecks('mlFilterListAddItemTextArea', keywords.join('\n'), { + clearWithKeyboard: true, + }); + await testSubjects.existOrFail('mlFilterListAddItemsButton'); + await this.assertAddItemsButtonEnabled(true); + await testSubjects.click('mlFilterListAddItemsButton'); + + for (let index = 0; index < keywords.length; index++) { + await this.assertFilterItemExists(keywords[index]); + } + }, + + async assertFilterListDescriptionValue(expectedDescription: string) { + const actualFilterListDescription = await testSubjects.getVisibleText( + 'mlNewFilterListDescriptionText' + ); + expect(actualFilterListDescription).to.eql( + expectedDescription, + `Filter list description should be '${expectedDescription}' (got '${actualFilterListDescription}')` + ); + }, + + async saveFilterList() { + await this.assertSaveFilterListButtonEnabled(true); + await testSubjects.click('mlFilterListSaveButton'); + await testSubjects.existOrFail('mlPageFilterListManagement'); + }, }; } From 42026cbbf54a7109b823567b689650066db36216 Mon Sep 17 00:00:00 2001 From: Constance Date: Tue, 22 Sep 2020 12:33:37 -0700 Subject: [PATCH 16/92] [Enterprise Search] Move http out of React Context and to Kea Logic; prefer directly mounting (#78167) * Remove HttpProvider in favor of mounting HttpLogic directly w/ props - removes need for initializeHttp call - ensures http value is loaded into HttpLogic as soon as possible / should never load in as null, reducing # of rerenders/checks see: https://kea.js.org/docs/guide/advanced#mounting-and-unmounting * Update simplest components using http for sendTelemetry * Update simplest tests for components using HttpLogic + default Kea mocks - Kea mock import should now contain mock default values which can be overridden * Update moderately complex tests using HttpLogic send_telemetry: - refactor to use shallow (w/ useEffect mocked) vs mount - check mockHttpValues directly engine_table: - refactor to use mount w/ an I18nProvider rather than mountWithContext helper (which we'll likely need to overhaul in the future) - assert mockHttpValues directly * Update EngineOverview to HttpLogic + refactors EngineOverview: - Change use of FormattedMessage to i18n.translate (simpler, no provider required) Tests: - Create mock values/actions for FlashMessages, since EngineOverview calls it - Create combined mockAllValues obj for easier overriding - Create setMockValues helper for easier test overriding (credit to @scottybollinger for the idea!) - Update engine_overview tests to setMockValues instead of passing context to mountWithAsyncContext - Fix mountWithAsyncContext to accept an undefined obj * Remove http from KibanaContext - it should now only live in HttpLogic :fire: * Remove FlashMessagesProvider in favor of mounting logic directly w/ props - send history as prop - refactor out now-unnecessary listenToHistory (we can just do it directly in afterMount without worrying about duplicate react rerenders) - add mount helper Tests: - refactor history.listen mock to mockHistory (so that set_message_helpers can use it as well) - use mountFlashMessagesLogic + create an even shorter mount() helper (credit to @JasonStoltz for the idea!) - refactor out DEFAULT_VALUES since we're not really using it anywhere else in the file, and it's not super applicable to this store - update history listener tests to account for logic occurring immediately on mount --- .../__mocks__/flash_messages_logic.mock.ts | 17 ++++++ .../applications/__mocks__/http_logic.mock.ts | 13 +++++ .../public/applications/__mocks__/index.ts | 4 ++ .../public/applications/__mocks__/kea.mock.ts | 39 ++++++++++--- .../__mocks__/kibana_context.mock.ts | 2 - .../__mocks__/mount_with_context.mock.tsx | 2 +- .../__mocks__/react_router_history.mock.ts | 1 + .../components/empty_state.test.tsx | 1 + .../components/empty_state.tsx | 4 +- .../components/header.test.tsx | 1 + .../engine_overview/components/header.tsx | 4 +- .../engine_overview/engine_overview.test.tsx | 24 +++----- .../engine_overview/engine_overview.tsx | 21 ++++--- .../engine_overview/engine_table.test.tsx | 53 ++++++++++------- .../engine_overview/engine_table.tsx | 4 +- .../product_card/product_card.test.tsx | 2 + .../components/product_card/product_card.tsx | 7 ++- .../public/applications/index.tsx | 30 +++++----- .../flash_messages_logic.test.ts | 58 +++++++++---------- .../flash_messages/flash_messages_logic.ts | 22 ++++--- .../flash_messages_provider.test.tsx | 46 --------------- .../flash_messages_provider.tsx | 26 --------- .../shared/flash_messages/index.ts | 2 +- .../set_message_helpers.test.ts | 5 +- .../shared/http/http_logic.test.ts | 37 +++++------- .../applications/shared/http/http_logic.ts | 39 ++++++++----- .../shared/http/http_provider.test.tsx | 45 -------------- .../shared/http/http_provider.tsx | 29 ---------- .../public/applications/shared/http/index.ts | 3 +- .../shared/telemetry/send_telemetry.test.tsx | 31 +++++----- .../shared/telemetry/send_telemetry.tsx | 11 ++-- .../product_button/product_button.test.tsx | 1 + .../shared/product_button/product_button.tsx | 4 +- .../views/overview/onboarding_card.test.tsx | 1 + .../views/overview/onboarding_card.tsx | 5 +- .../views/overview/onboarding_steps.tsx | 3 +- .../views/overview/recent_activity.tsx | 3 +- 37 files changed, 268 insertions(+), 332 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/__mocks__/http_logic.mock.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.ts new file mode 100644 index 0000000000000..a610ea0238ac0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export const mockFlashMessagesValues = { + messages: [], + queuedMessages: [], +}; + +export const mockFlashMessagesActions = { + setFlashMessages: jest.fn(), + clearFlashMessages: jest.fn(), + setQueuedMessages: jest.fn(), + clearQueuedMessages: jest.fn(), +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/http_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/http_logic.mock.ts new file mode 100644 index 0000000000000..e77863c70c23a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/http_logic.mock.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServiceMock } from 'src/core/public/mocks'; + +export const mockHttpValues = { + http: httpServiceMock.createSetupContract(), + errorConnecting: false, + readOnlyMode: false, +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts index e999d40a3f8e6..f66235ff44c6a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts @@ -7,6 +7,10 @@ export { mockHistory, mockLocation } from './react_router_history.mock'; export { mockKibanaContext } from './kibana_context.mock'; export { mockLicenseContext } from './license_context.mock'; +export { mockHttpValues } from './http_logic.mock'; +export { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock'; +export { mockAllValues, mockAllActions, setMockValues } from './kea.mock'; + export { mountWithContext, mountWithKibanaContext, diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts index 5049e9da21ce9..8e6b0baa5fc00 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts @@ -4,21 +4,46 @@ * you may not use this file except in compliance with the Elastic License. */ +/** + * NOTE: These variable names MUST start with 'mock*' in order for + * Jest to accept its use within a jest.mock() + */ +import { mockHttpValues } from './http_logic.mock'; +import { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock'; + +export const mockAllValues = { + ...mockHttpValues, + ...mockFlashMessagesValues, +}; +export const mockAllActions = { + ...mockFlashMessagesActions, +}; + +/** + * Import this file directly to mock useValues with a set of default values for all shared logic files. + * Example usage: + * + * import '../../../__mocks__/kea'; // Must come before kea's import, adjust relative path as needed + */ jest.mock('kea', () => ({ ...(jest.requireActual('kea') as object), - useValues: jest.fn(() => ({})), - useActions: jest.fn(() => ({})), + useValues: jest.fn(() => ({ ...mockAllValues })), + useActions: jest.fn(() => ({ ...mockAllActions })), })); /** + * Call this function to override a specific set of Kea values while retaining all other defaults * Example usage within a component test: * - * import '../../../__mocks__/kea'; // Must come before kea's import, adjust relative path as needed - * - * import { useActions, useValues } from 'kea'; + * import '../../../__mocks__/kea'; + * import { setMockValues } from ''../../../__mocks__'; * * it('some test', () => { - * (useValues as jest.Mock).mockImplementationOnce(() => ({ someValue: 'hello' })); - * (useActions as jest.Mock).mockImplementationOnce(() => ({ someAction: () => 'world' })); + * setMockValues({ someValue: 'hello' }); * }); */ +import { useValues } from 'kea'; + +export const setMockValues = (values: object) => { + (useValues as jest.Mock).mockImplementation(() => ({ ...mockAllValues, ...values })); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts index 890072ab42eb9..ea3c3923cc472 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { httpServiceMock } from 'src/core/public/mocks'; import { ExternalUrl } from '../shared/enterprise_search_url'; /** @@ -12,7 +11,6 @@ import { ExternalUrl } from '../shared/enterprise_search_url'; * @see enterprise_search/public/index.tsx for the KibanaContext definition/import */ export const mockKibanaContext = { - http: httpServiceMock.createSetupContract(), navigateToUrl: jest.fn(), setBreadcrumbs: jest.fn(), setDocTitle: jest.fn(), diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx index 826e0482acef7..5e56f17c8e7f3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx @@ -67,7 +67,7 @@ export const mountWithKibanaContext = (children: React.ReactNode, context?: obje */ export const mountWithAsyncContext = async ( children: React.ReactNode, - context: object + context?: object ): Promise => { let wrapper: ReactWrapper | undefined; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts index 842dcefd3aef8..7b3ac86ad0ab1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts @@ -14,6 +14,7 @@ export const mockHistory = { location: { pathname: '/current-path', }, + listen: jest.fn(() => jest.fn()), }; export const mockLocation = { key: 'someKey', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx index 7e6876bc9b3a4..233db7d4c5917 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../../../__mocks__/kea.mock'; import '../../../../__mocks__/shallow_usecontext.mock'; import React from 'react'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx index 58691cf09b4a5..5ed1f0b277306 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx @@ -5,10 +5,12 @@ */ import React, { useContext } from 'react'; +import { useValues } from 'kea'; import { EuiPageContent, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { sendTelemetry } from '../../../../shared/telemetry'; +import { HttpLogic } from '../../../../shared/http'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; import { KibanaContext, IKibanaContext } from '../../../../index'; import { CREATE_ENGINES_PATH } from '../../../routes'; @@ -18,9 +20,9 @@ import { EngineOverviewHeader } from './header'; import './empty_state.scss'; export const EmptyState: React.FC = () => { + const { http } = useValues(HttpLogic); const { externalUrl: { getAppSearchUrl }, - http, } = useContext(KibanaContext) as IKibanaContext; const buttonProps = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx index 7f22ce132d405..8c7dfa2b7c3d6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../../../__mocks__/kea.mock'; import '../../../../__mocks__/shallow_usecontext.mock'; import React from 'react'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx index 1a1ae295d4828..dca0d45a207b4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx @@ -5,6 +5,7 @@ */ import React, { useContext } from 'react'; +import { useValues } from 'kea'; import { EuiPageHeader, EuiPageHeaderSection, @@ -16,12 +17,13 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { sendTelemetry } from '../../../../shared/telemetry'; +import { HttpLogic } from '../../../../shared/http'; import { KibanaContext, IKibanaContext } from '../../../../index'; export const EngineOverviewHeader: React.FC = () => { + const { http } = useValues(HttpLogic); const { externalUrl: { getAppSearchUrl }, - http, } = useContext(KibanaContext) as IKibanaContext; const buttonProps = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx index c2379fb33bd71..928d92d791094 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../../__mocks__/kea.mock'; import '../../../__mocks__/react_router_history.mock'; import React from 'react'; import { act } from 'react-dom/test-utils'; import { shallow, ReactWrapper } from 'enzyme'; -import { mountWithAsyncContext, mockKibanaContext } from '../../../__mocks__'; +import { mountWithAsyncContext, mockHttpValues, setMockValues } from '../../../__mocks__'; import { LoadingState, EmptyState } from './components'; import { EngineTable } from './engine_table'; @@ -18,8 +19,6 @@ import { EngineTable } from './engine_table'; import { EngineOverview } from './'; describe('EngineOverview', () => { - const mockHttp = mockKibanaContext.http; - describe('non-happy-path states', () => { it('isLoading', () => { const wrapper = shallow(); @@ -28,15 +27,16 @@ describe('EngineOverview', () => { }); it('isEmpty', async () => { - const wrapper = await mountWithAsyncContext(, { + setMockValues({ http: { - ...mockHttp, + ...mockHttpValues.http, get: () => ({ results: [], meta: { page: { total_results: 0 } }, }), }, }); + const wrapper = await mountWithAsyncContext(); expect(wrapper.find(EmptyState)).toHaveLength(1); }); @@ -65,12 +65,11 @@ describe('EngineOverview', () => { beforeEach(() => { jest.clearAllMocks(); + setMockValues({ http: { ...mockHttpValues.http, get: mockApi } }); }); it('renders and calls the engines API', async () => { - const wrapper = await mountWithAsyncContext(, { - http: { ...mockHttp, get: mockApi }, - }); + const wrapper = await mountWithAsyncContext(); expect(wrapper.find(EngineTable)).toHaveLength(1); expect(mockApi).toHaveBeenNthCalledWith(1, '/api/app_search/engines', { @@ -84,7 +83,6 @@ describe('EngineOverview', () => { describe('when on a platinum license', () => { it('renders a 2nd meta engines table & makes a 2nd meta engines API call', async () => { const wrapper = await mountWithAsyncContext(, { - http: { ...mockHttp, get: mockApi }, license: { type: 'platinum', isActive: true }, }); @@ -103,9 +101,7 @@ describe('EngineOverview', () => { wrapper.find(EngineTable).prop('pagination'); it('passes down page data from the API', async () => { - const wrapper = await mountWithAsyncContext(, { - http: { ...mockHttp, get: mockApi }, - }); + const wrapper = await mountWithAsyncContext(); const pagination = getTablePagination(wrapper); expect(pagination.totalEngines).toEqual(100); @@ -113,9 +109,7 @@ describe('EngineOverview', () => { }); it('re-polls the API on page change', async () => { - const wrapper = await mountWithAsyncContext(, { - http: { ...mockHttp, get: mockApi }, - }); + const wrapper = await mountWithAsyncContext(); await act(async () => getTablePagination(wrapper).onPaginate(5)); wrapper.update(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx index 9703fde7e140a..c0aedbe7dc6b4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx @@ -5,6 +5,7 @@ */ import React, { useContext, useEffect, useState } from 'react'; +import { useValues } from 'kea'; import { EuiPageContent, EuiPageContentHeader, @@ -12,13 +13,13 @@ import { EuiTitle, EuiSpacer, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { FlashMessages } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; import { LicenseContext, ILicenseContext, hasPlatinumLicense } from '../../../shared/licensing'; -import { KibanaContext, IKibanaContext } from '../../../index'; import { EngineIcon } from './assets/engine_icon'; import { MetaEngineIcon } from './assets/meta_engine_icon'; @@ -38,7 +39,7 @@ interface ISetEnginesCallbacks { } export const EngineOverview: React.FC = () => { - const { http } = useContext(KibanaContext) as IKibanaContext; + const { http } = useValues(HttpLogic); const { license } = useContext(LicenseContext) as ILicenseContext; const [isLoading, setIsLoading] = useState(true); @@ -94,10 +95,9 @@ export const EngineOverview: React.FC = () => {

- + {i18n.translate('xpack.enterpriseSearch.appSearch.enginesOverview.engines', { + defaultMessage: 'Engines', + })}

@@ -119,10 +119,9 @@ export const EngineOverview: React.FC = () => {

- + {i18n.translate('xpack.enterpriseSearch.appSearch.enginesOverview.metaEngines', { + defaultMessage: 'Meta Engines', + })}

diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx index 46b6e61e352de..8e92f21f8ffed 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx @@ -4,10 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../../__mocks__/kea.mock'; +import '../../../__mocks__/shallow_usecontext.mock'; +import { mockHttpValues } from '../../../__mocks__/'; + import React from 'react'; +import { mount } from 'enzyme'; +import { I18nProvider } from '@kbn/i18n/react'; import { EuiBasicTable, EuiPagination, EuiButtonEmpty, EuiLink } from '@elastic/eui'; -import { mountWithContext } from '../../../__mocks__'; jest.mock('../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() })); import { sendTelemetry } from '../../../shared/telemetry'; @@ -16,22 +21,24 @@ import { EngineTable } from './engine_table'; describe('EngineTable', () => { const onPaginate = jest.fn(); // onPaginate updates the engines API call upstream - const wrapper = mountWithContext( - + const wrapper = mount( + + + ); const table = wrapper.find(EuiBasicTable); @@ -56,7 +63,7 @@ describe('EngineTable', () => { link.simulate('click'); expect(sendTelemetry).toHaveBeenCalledWith({ - http: expect.any(Object), + http: mockHttpValues.http, product: 'app_search', action: 'clicked', metric: 'engine_table_link', @@ -71,10 +78,16 @@ describe('EngineTable', () => { }); it('handles empty data', () => { - const emptyWrapper = mountWithContext( - {} }} /> + const emptyWrapper = mount( + + {} }} + /> + ); const emptyTable = emptyWrapper.find(EuiBasicTable); + expect(emptyTable.prop('pagination').pageIndex).toEqual(0); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx index 9c6122c88c7d7..6888be1dc2b5b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx @@ -5,11 +5,13 @@ */ import React, { useContext } from 'react'; +import { useValues } from 'kea'; import { EuiBasicTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui'; import { FormattedMessage, FormattedDate, FormattedNumber } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { sendTelemetry } from '../../../shared/telemetry'; +import { HttpLogic } from '../../../shared/http'; import { KibanaContext, IKibanaContext } from '../../../index'; import { getEngineRoute } from '../../routes'; @@ -40,9 +42,9 @@ export const EngineTable: React.FC = ({ data, pagination: { totalEngines, pageIndex, onPaginate }, }) => { + const { http } = useValues(HttpLogic); const { externalUrl: { getAppSearchUrl }, - http, } = useContext(KibanaContext) as IKibanaContext; const engineLinkProps = (name: string) => ({ diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx index a76b654ccddd0..f651511e61b44 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../../__mocks__/kea.mock'; + import React from 'react'; import { shallow } from 'enzyme'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx index 334ca126cabb9..833a782a32f00 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; +import { useValues } from 'kea'; import upperFirst from 'lodash/upperFirst'; import snakeCase from 'lodash/snakeCase'; import { i18n } from '@kbn/i18n'; @@ -12,7 +13,7 @@ import { EuiCard, EuiTextColor } from '@elastic/eui'; import { EuiButton } from '../../../shared/react_router_helpers'; import { sendTelemetry } from '../../../shared/telemetry'; -import { KibanaContext, IKibanaContext } from '../../../index'; +import { HttpLogic } from '../../../shared/http'; import './product_card.scss'; @@ -28,7 +29,7 @@ interface IProductCard { } export const ProductCard: React.FC = ({ product, image }) => { - const { http } = useContext(KibanaContext) as IKibanaContext; + const { http } = useValues(HttpLogic); return ( - - @@ -86,6 +80,8 @@ export const renderApp = ( ); return () => { ReactDOM.unmountComponentAtNode(params.element); + unmountHttpLogic(); + unmountFlashMessagesLogic(); }; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts index 136912847baa9..c12011b47a472 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts @@ -6,23 +6,25 @@ import { resetContext } from 'kea'; -import { FlashMessagesLogic, IFlashMessage } from './flash_messages_logic'; +import { mockHistory } from '../../__mocks__'; + +import { FlashMessagesLogic, mountFlashMessagesLogic, IFlashMessage } from './'; describe('FlashMessagesLogic', () => { - const DEFAULT_VALUES = { - messages: [], - queuedMessages: [], - historyListener: null, - }; + const mount = () => mountFlashMessagesLogic({ history: mockHistory as any }); beforeEach(() => { jest.clearAllMocks(); resetContext({}); }); - it('has expected default values', () => { - FlashMessagesLogic.mount(); - expect(FlashMessagesLogic.values).toEqual(DEFAULT_VALUES); + it('has default values', () => { + mount(); + expect(FlashMessagesLogic.values).toEqual({ + messages: [], + queuedMessages: [], + historyListener: expect.any(Function), + }); }); describe('setFlashMessages()', () => { @@ -33,7 +35,7 @@ describe('FlashMessagesLogic', () => { { type: 'info', message: 'Everything is fine, nothing is ruined' }, ]; - FlashMessagesLogic.mount(); + mount(); FlashMessagesLogic.actions.setFlashMessages(messages); expect(FlashMessagesLogic.values.messages).toEqual(messages); @@ -42,7 +44,7 @@ describe('FlashMessagesLogic', () => { it('automatically converts to an array if a single message obj is passed in', () => { const message = { type: 'success', message: 'I turn into an array!' } as IFlashMessage; - FlashMessagesLogic.mount(); + mount(); FlashMessagesLogic.actions.setFlashMessages(message); expect(FlashMessagesLogic.values.messages).toEqual([message]); @@ -51,7 +53,7 @@ describe('FlashMessagesLogic', () => { describe('clearFlashMessages()', () => { it('sets messages back to an empty array', () => { - FlashMessagesLogic.mount(); + mount(); FlashMessagesLogic.actions.setFlashMessages('test' as any); FlashMessagesLogic.actions.clearFlashMessages(); @@ -63,7 +65,7 @@ describe('FlashMessagesLogic', () => { it('sets an array of messages', () => { const queuedMessage: IFlashMessage = { type: 'error', message: 'You deleted a thing' }; - FlashMessagesLogic.mount(); + mount(); FlashMessagesLogic.actions.setQueuedMessages(queuedMessage); expect(FlashMessagesLogic.values.queuedMessages).toEqual([queuedMessage]); @@ -72,7 +74,7 @@ describe('FlashMessagesLogic', () => { describe('clearQueuedMessages()', () => { it('sets queued messages back to an empty array', () => { - FlashMessagesLogic.mount(); + mount(); FlashMessagesLogic.actions.setQueuedMessages('test' as any); FlashMessagesLogic.actions.clearQueuedMessages(); @@ -83,30 +85,25 @@ describe('FlashMessagesLogic', () => { describe('history listener logic', () => { describe('setHistoryListener()', () => { it('sets the historyListener value', () => { - FlashMessagesLogic.mount(); + mount(); FlashMessagesLogic.actions.setHistoryListener('test' as any); expect(FlashMessagesLogic.values.historyListener).toEqual('test'); }); }); - describe('listenToHistory()', () => { + describe('on mount', () => { it('listens for history changes and clears messages on change', () => { - FlashMessagesLogic.mount(); + mount(); + expect(mockHistory.listen).toHaveBeenCalled(); + FlashMessagesLogic.actions.setQueuedMessages(['queuedMessages'] as any); jest.spyOn(FlashMessagesLogic.actions, 'clearFlashMessages'); jest.spyOn(FlashMessagesLogic.actions, 'setFlashMessages'); jest.spyOn(FlashMessagesLogic.actions, 'clearQueuedMessages'); jest.spyOn(FlashMessagesLogic.actions, 'setHistoryListener'); - const mockListener = jest.fn(() => jest.fn()); - const history = { listen: mockListener } as any; - FlashMessagesLogic.actions.listenToHistory(history); - - expect(mockListener).toHaveBeenCalled(); - expect(FlashMessagesLogic.actions.setHistoryListener).toHaveBeenCalled(); - - const mockHistoryChange = (mockListener.mock.calls[0] as any)[0]; + const mockHistoryChange = (mockHistory.listen.mock.calls[0] as any)[0]; mockHistoryChange(); expect(FlashMessagesLogic.actions.clearFlashMessages).toHaveBeenCalled(); expect(FlashMessagesLogic.actions.setFlashMessages).toHaveBeenCalledWith([ @@ -116,19 +113,20 @@ describe('FlashMessagesLogic', () => { }); }); - describe('beforeUnmount', () => { - it('removes history listener on unmount', () => { + describe('on unmount', () => { + it('removes history listener', () => { const mockUnlistener = jest.fn(); - const unmount = FlashMessagesLogic.mount(); + mockHistory.listen.mockReturnValueOnce(mockUnlistener); - FlashMessagesLogic.actions.setHistoryListener(mockUnlistener); + const unmount = mount(); unmount(); expect(mockUnlistener).toHaveBeenCalled(); }); it('does not crash if no listener exists', () => { - const unmount = FlashMessagesLogic.mount(); + const unmount = mount(); + FlashMessagesLogic.actions.setHistoryListener(null as any); unmount(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts index 37a8f16acad6d..1735cc8ac7228 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts @@ -24,7 +24,6 @@ export interface IFlashMessagesActions { clearFlashMessages(): void; setQueuedMessages(messages: IFlashMessage | IFlashMessage[]): { messages: IFlashMessage[] }; clearQueuedMessages(): void; - listenToHistory(history: History): History; setHistoryListener(historyListener: Function): { historyListener: Function }; } @@ -38,7 +37,6 @@ export const FlashMessagesLogic = kea null, setQueuedMessages: (messages) => ({ messages: convertToArray(messages) }), clearQueuedMessages: () => null, - listenToHistory: (history) => history, setHistoryListener: (historyListener) => ({ historyListener }), }, reducers: { @@ -63,21 +61,31 @@ export const FlashMessagesLogic = kea ({ - listenToHistory: (history) => { + events: ({ props, values, actions }) => ({ + afterMount: () => { // On React Router navigation, clear previous flash messages and load any queued messages - const unlisten = history.listen(() => { + const unlisten = props.history.listen(() => { actions.clearFlashMessages(); actions.setFlashMessages(values.queuedMessages); actions.clearQueuedMessages(); }); actions.setHistoryListener(unlisten); }, - }), - events: ({ values }) => ({ beforeUnmount: () => { const { historyListener: removeHistoryListener } = values; if (removeHistoryListener) removeHistoryListener(); }, }), }); + +/** + * Mount/props helper + */ +interface IFlashMessagesLogicProps { + history: History; +} +export const mountFlashMessagesLogic = (props: IFlashMessagesLogicProps) => { + FlashMessagesLogic(props); + const unmount = FlashMessagesLogic.mount(); + return unmount; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.test.tsx deleted file mode 100644 index bcd7abd6d7ce2..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.test.tsx +++ /dev/null @@ -1,46 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import '../../__mocks__/shallow_usecontext.mock'; -import '../../__mocks__/kea.mock'; - -import React from 'react'; -import { shallow } from 'enzyme'; -import { useValues, useActions } from 'kea'; - -import { mockHistory } from '../../__mocks__'; - -import { FlashMessagesProvider } from './'; - -describe('FlashMessagesProvider', () => { - const props = { history: mockHistory as any }; - const listenToHistory = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - (useActions as jest.Mock).mockImplementationOnce(() => ({ listenToHistory })); - }); - - it('does not render', () => { - const wrapper = shallow(); - - expect(wrapper.isEmptyRender()).toBe(true); - }); - - it('listens to history on mount', () => { - shallow(); - - expect(listenToHistory).toHaveBeenCalledWith(mockHistory); - }); - - it('does not add another history listener if one already exists', () => { - (useValues as jest.Mock).mockImplementationOnce(() => ({ historyListener: 'exists' as any })); - - shallow(); - - expect(listenToHistory).not.toHaveBeenCalledWith(props); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx deleted file mode 100644 index a3ceabcf6ac8a..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useEffect } from 'react'; -import { useValues, useActions } from 'kea'; -import { History } from 'history'; - -import { FlashMessagesLogic } from './flash_messages_logic'; - -interface IFlashMessagesProviderProps { - history: History; -} - -export const FlashMessagesProvider: React.FC = ({ history }) => { - const { historyListener } = useValues(FlashMessagesLogic); - const { listenToHistory } = useActions(FlashMessagesLogic); - - useEffect(() => { - if (!historyListener) listenToHistory(history); - }, []); - - return null; -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts index c4daeb44420c8..21c1a60efa6b7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts @@ -10,7 +10,7 @@ export { IFlashMessage, IFlashMessagesValues, IFlashMessagesActions, + mountFlashMessagesLogic, } from './flash_messages_logic'; -export { FlashMessagesProvider } from './flash_messages_provider'; export { flashAPIErrors } from './handle_api_errors'; export { setSuccessMessage, setErrorMessage, setQueuedSuccessMessage } from './set_message_helpers'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts index c3c60d77f4577..f2ddd560ac9c1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { mockHistory } from '../../__mocks__'; + import { FlashMessagesLogic, + mountFlashMessagesLogic, setSuccessMessage, setErrorMessage, setQueuedSuccessMessage, @@ -15,7 +18,7 @@ describe('Flash Message Helpers', () => { const message = 'I am a message'; beforeEach(() => { - FlashMessagesLogic.mount(); + mountFlashMessagesLogic({ history: mockHistory as any }); }); it('setSuccessMessage()', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts index b65499be2f7c0..df32b5496c367 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts @@ -8,31 +8,20 @@ import { resetContext } from 'kea'; import { httpServiceMock } from 'src/core/public/mocks'; -import { HttpLogic } from './http_logic'; +import { HttpLogic, mountHttpLogic } from './http_logic'; describe('HttpLogic', () => { const mockHttp = httpServiceMock.createSetupContract(); - const DEFAULT_VALUES = { - http: null, - httpInterceptors: [], - errorConnecting: false, - readOnlyMode: false, - }; + const mount = () => mountHttpLogic({ http: mockHttp }); beforeEach(() => { jest.clearAllMocks(); resetContext({}); }); - it('has expected default values', () => { - HttpLogic.mount(); - expect(HttpLogic.values).toEqual(DEFAULT_VALUES); - }); - - describe('initializeHttp()', () => { - it('sets values based on passed props', () => { - HttpLogic.mount(); - HttpLogic.actions.initializeHttp({ + describe('mounts', () => { + it('sets values from props', () => { + mountHttpLogic({ http: mockHttp, errorConnecting: true, readOnlyMode: true, @@ -40,7 +29,7 @@ describe('HttpLogic', () => { expect(HttpLogic.values).toEqual({ http: mockHttp, - httpInterceptors: [], + httpInterceptors: expect.any(Array), errorConnecting: true, readOnlyMode: true, }); @@ -49,7 +38,9 @@ describe('HttpLogic', () => { describe('setErrorConnecting()', () => { it('sets errorConnecting value', () => { - HttpLogic.mount(); + mount(); + expect(HttpLogic.values.errorConnecting).toEqual(false); + HttpLogic.actions.setErrorConnecting(true); expect(HttpLogic.values.errorConnecting).toEqual(true); @@ -60,7 +51,9 @@ describe('HttpLogic', () => { describe('setReadOnlyMode()', () => { it('sets readOnlyMode value', () => { - HttpLogic.mount(); + mount(); + expect(HttpLogic.values.readOnlyMode).toEqual(false); + HttpLogic.actions.setReadOnlyMode(true); expect(HttpLogic.values.readOnlyMode).toEqual(true); @@ -72,10 +65,8 @@ describe('HttpLogic', () => { describe('http interceptors', () => { describe('initializeHttpInterceptors()', () => { beforeEach(() => { - HttpLogic.mount(); + mount(); jest.spyOn(HttpLogic.actions, 'setHttpInterceptors'); - HttpLogic.actions.initializeHttp({ http: mockHttp }); - HttpLogic.actions.initializeHttpInterceptors(); }); it('calls http.intercept and sets an array of interceptors', () => { @@ -165,7 +156,7 @@ describe('HttpLogic', () => { }); it('sets httpInterceptors and calls all valid remove functions on unmount', () => { - const unmount = HttpLogic.mount(); + const unmount = mount(); const httpInterceptors = [jest.fn(), undefined, jest.fn()] as any; HttpLogic.actions.setHttpInterceptors(httpInterceptors); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts index 72380142fe399..d16e507bfb3bc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts @@ -7,7 +7,6 @@ import { kea, MakeLogicType } from 'kea'; import { HttpSetup, HttpInterceptorResponseError, HttpResponse } from 'src/core/public'; -import { IHttpProviderProps } from './http_provider'; import { READ_ONLY_MODE_HEADER } from '../../../../common/constants'; @@ -18,7 +17,6 @@ export interface IHttpValues { readOnlyMode: boolean; } export interface IHttpActions { - initializeHttp({ http, errorConnecting, readOnlyMode }: IHttpProviderProps): IHttpProviderProps; initializeHttpInterceptors(): void; setHttpInterceptors(httpInterceptors: Function[]): { httpInterceptors: Function[] }; setErrorConnecting(errorConnecting: boolean): { errorConnecting: boolean }; @@ -28,19 +26,13 @@ export interface IHttpActions { export const HttpLogic = kea>({ path: ['enterprise_search', 'http_logic'], actions: { - initializeHttp: (props) => props, initializeHttpInterceptors: () => null, setHttpInterceptors: (httpInterceptors) => ({ httpInterceptors }), setErrorConnecting: (errorConnecting) => ({ errorConnecting }), setReadOnlyMode: (readOnlyMode) => ({ readOnlyMode }), }, - reducers: { - http: [ - (null as unknown) as HttpSetup, - { - initializeHttp: (_, { http }) => http, - }, - ], + reducers: ({ props }) => ({ + http: [props.http, {}], httpInterceptors: [ [], { @@ -48,20 +40,18 @@ export const HttpLogic = kea>({ }, ], errorConnecting: [ - false, + props.errorConnecting || false, { - initializeHttp: (_, { errorConnecting }) => !!errorConnecting, setErrorConnecting: (_, { errorConnecting }) => errorConnecting, }, ], readOnlyMode: [ - false, + props.readOnlyMode || false, { - initializeHttp: (_, { readOnlyMode }) => !!readOnlyMode, setReadOnlyMode: (_, { readOnlyMode }) => readOnlyMode, }, ], - }, + }), listeners: ({ values, actions }) => ({ initializeHttpInterceptors: () => { const httpInterceptors = []; @@ -103,7 +93,10 @@ export const HttpLogic = kea>({ actions.setHttpInterceptors(httpInterceptors); }, }), - events: ({ values }) => ({ + events: ({ values, actions }) => ({ + afterMount: () => { + actions.initializeHttpInterceptors(); + }, beforeUnmount: () => { values.httpInterceptors.forEach((removeInterceptorFn?: Function) => { if (removeInterceptorFn) removeInterceptorFn(); @@ -112,6 +105,20 @@ export const HttpLogic = kea>({ }), }); +/** + * Mount/props helper + */ +interface IHttpLogicProps { + http: HttpSetup; + errorConnecting?: boolean; + readOnlyMode?: boolean; +} +export const mountHttpLogic = (props: IHttpLogicProps) => { + HttpLogic(props); + const unmount = HttpLogic.mount(); + return unmount; +}; + /** * Small helper that checks whether or not an http call is for an Enterprise Search API */ diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx deleted file mode 100644 index 902c910f10d7c..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx +++ /dev/null @@ -1,45 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import '../../__mocks__/shallow_usecontext.mock'; -import '../../__mocks__/kea.mock'; - -import React from 'react'; -import { shallow } from 'enzyme'; -import { useActions } from 'kea'; - -import { HttpProvider } from './'; - -describe('HttpProvider', () => { - const props = { - http: {} as any, - errorConnecting: false, - readOnlyMode: false, - }; - const initializeHttp = jest.fn(); - const initializeHttpInterceptors = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - (useActions as jest.Mock).mockImplementationOnce(() => ({ - initializeHttp, - initializeHttpInterceptors, - })); - }); - - it('does not render', () => { - const wrapper = shallow(); - - expect(wrapper.isEmptyRender()).toBe(true); - }); - - it('calls initialization actions on mount', () => { - shallow(); - - expect(initializeHttp).toHaveBeenCalledWith(props); - expect(initializeHttpInterceptors).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx deleted file mode 100644 index db1b0d611079a..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useEffect } from 'react'; -import { useActions } from 'kea'; - -import { HttpSetup } from 'src/core/public'; - -import { HttpLogic } from './http_logic'; - -export interface IHttpProviderProps { - http: HttpSetup; - errorConnecting?: boolean; - readOnlyMode?: boolean; -} - -export const HttpProvider: React.FC = (props) => { - const { initializeHttp, initializeHttpInterceptors } = useActions(HttpLogic); - - useEffect(() => { - initializeHttp(props); - initializeHttpInterceptors(); - }, []); - - return null; -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts index db65e80ca25c2..46a52415f8564 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { HttpLogic, IHttpValues, IHttpActions } from './http_logic'; -export { HttpProvider } from './http_provider'; +export { HttpLogic, IHttpValues, IHttpActions, mountHttpLogic } from './http_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx index 1d64b453b2c2c..073c548ba47fa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../__mocks__/kea.mock'; +import '../../__mocks__/shallow_usecontext.mock'; +import { mockHttpValues } from '../../__mocks__'; + import React from 'react'; +import { shallow } from 'enzyme'; -import { httpServiceMock } from 'src/core/public/mocks'; import { JSON_HEADER as headers } from '../../../../common/constants'; -import { mountWithKibanaContext } from '../../__mocks__'; import { sendTelemetry, @@ -18,8 +21,6 @@ import { } from './'; describe('Shared Telemetry Helpers', () => { - const httpMock = httpServiceMock.createSetupContract(); - beforeEach(() => { jest.clearAllMocks(); }); @@ -27,13 +28,13 @@ describe('Shared Telemetry Helpers', () => { describe('sendTelemetry', () => { it('successfully calls the server-side telemetry endpoint', () => { sendTelemetry({ - http: httpMock, + http: mockHttpValues.http, product: 'enterprise_search', action: 'viewed', metric: 'setup_guide', }); - expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { + expect(mockHttpValues.http.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { headers, body: '{"product":"enterprise_search","action":"viewed","metric":"setup_guide"}', }); @@ -50,33 +51,27 @@ describe('Shared Telemetry Helpers', () => { describe('React component helpers', () => { it('SendEnterpriseSearchTelemetry component', () => { - mountWithKibanaContext(, { - http: httpMock, - }); + shallow(); - expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { + expect(mockHttpValues.http.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { headers, body: '{"product":"enterprise_search","action":"viewed","metric":"page"}', }); }); it('SendAppSearchTelemetry component', () => { - mountWithKibanaContext(, { - http: httpMock, - }); + shallow(); - expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { + expect(mockHttpValues.http.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { headers, body: '{"product":"app_search","action":"clicked","metric":"button"}', }); }); it('SendWorkplaceSearchTelemetry component', () => { - mountWithKibanaContext(, { - http: httpMock, - }); + shallow(); - expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { + expect(mockHttpValues.http.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { headers, body: '{"product":"workplace_search","action":"error","metric":"not_found"}', }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx index e3c9ba9b8a218..2f87597897b41 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useEffect } from 'react'; +import React, { useEffect } from 'react'; +import { useValues } from 'kea'; import { HttpSetup } from 'src/core/public'; import { JSON_HEADER as headers } from '../../../../common/constants'; -import { KibanaContext, IKibanaContext } from '../../index'; +import { HttpLogic } from '../http'; interface ISendTelemetryProps { action: 'viewed' | 'error' | 'clicked'; @@ -41,7 +42,7 @@ export const SendEnterpriseSearchTelemetry: React.FC = ({ action, metric, }) => { - const { http } = useContext(KibanaContext) as IKibanaContext; + const { http } = useValues(HttpLogic); useEffect(() => { sendTelemetry({ http, action, metric, product: 'enterprise_search' }); @@ -51,7 +52,7 @@ export const SendEnterpriseSearchTelemetry: React.FC = ({ }; export const SendAppSearchTelemetry: React.FC = ({ action, metric }) => { - const { http } = useContext(KibanaContext) as IKibanaContext; + const { http } = useValues(HttpLogic); useEffect(() => { sendTelemetry({ http, action, metric, product: 'app_search' }); @@ -61,7 +62,7 @@ export const SendAppSearchTelemetry: React.FC = ({ action, }; export const SendWorkplaceSearchTelemetry: React.FC = ({ action, metric }) => { - const { http } = useContext(KibanaContext) as IKibanaContext; + const { http } = useValues(HttpLogic); useEffect(() => { sendTelemetry({ http, action, metric, product: 'workplace_search' }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx index 429a2c509813d..c73eb05ccec16 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../../../__mocks__/kea.mock'; import '../../../../__mocks__/shallow_usecontext.mock'; import React from 'react'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx index a914000654165..a80de9fd6ac82 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx @@ -5,17 +5,19 @@ */ import React, { useContext } from 'react'; +import { useValues } from 'kea'; import { EuiButton, EuiButtonProps, EuiLinkProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { sendTelemetry } from '../../../../shared/telemetry'; +import { HttpLogic } from '../../../../shared/http'; import { KibanaContext, IKibanaContext } from '../../../../index'; export const ProductButton: React.FC = () => { + const { http } = useValues(HttpLogic); const { externalUrl: { getWorkplaceSearchUrl }, - http, } = useContext(KibanaContext) as IKibanaContext; const buttonProps = { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx index 1d7c565935e97..c890adb8ea043 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../../__mocks__/kea.mock'; import '../../../__mocks__/shallow_usecontext.mock'; import React from 'react'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx index 786357358dfa6..79be7ef1cb158 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx @@ -5,6 +5,7 @@ */ import React, { useContext } from 'react'; +import { useValues } from 'kea'; import { EuiButton, @@ -17,7 +18,9 @@ import { EuiButtonEmptyProps, EuiLinkProps, } from '@elastic/eui'; + import { sendTelemetry } from '../../../shared/telemetry'; +import { HttpLogic } from '../../../shared/http'; import { KibanaContext, IKibanaContext } from '../../../index'; interface IOnboardingCardProps { @@ -39,8 +42,8 @@ export const OnboardingCard: React.FC = ({ actionPath, complete, }) => { + const { http } = useValues(HttpLogic); const { - http, externalUrl: { getWorkplaceSearchUrl }, } = useContext(KibanaContext) as IKibanaContext; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx index 0baadfc912ad5..079d981533e01 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx @@ -23,6 +23,7 @@ import { } from '@elastic/eui'; import sharedSourcesIcon from '../../components/shared/assets/share_circle.svg'; import { sendTelemetry } from '../../../shared/telemetry'; +import { HttpLogic } from '../../../shared/http'; import { KibanaContext, IKibanaContext } from '../../../index'; import { ORG_SOURCES_PATH, USERS_PATH, ORG_SETTINGS_PATH } from '../../routes'; @@ -135,8 +136,8 @@ export const OnboardingSteps: React.FC = () => { }; export const OrgNameOnboarding: React.FC = () => { + const { http } = useValues(HttpLogic); const { - http, externalUrl: { getWorkplaceSearchUrl }, } = useContext(KibanaContext) as IKibanaContext; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx index 0813999c9a078..dd62e6de7c046 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx @@ -14,6 +14,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ContentSection } from '../../components/shared/content_section'; import { sendTelemetry } from '../../../shared/telemetry'; +import { HttpLogic } from '../../../shared/http'; import { KibanaContext, IKibanaContext } from '../../../index'; import { SOURCE_DETAILS_PATH, getContentSourcePath } from '../../routes'; @@ -93,8 +94,8 @@ export const RecentActivityItem: React.FC = ({ timestamp, sourceId, }) => { + const { http } = useValues(HttpLogic); const { - http, externalUrl: { getWorkplaceSearchUrl }, } = useContext(KibanaContext) as IKibanaContext; From a537f9af500bc3d3a6e2ceea8817ee89c474cbb0 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 22 Sep 2020 12:33:59 -0700 Subject: [PATCH 17/92] [Reporting] Clean Up TypeScript Definitions (#76566) * [Reporting] Simplify Export Type Definitions, use defaults for generics, refactor * ReportApiJSON interface for common * rename JobSummary to JobStatusBucket for clarity * revert unneeded create mock changes * clean up the diff * revert changes to worker.js * rewrite comment * rename type to jobtype in JobStatusBucket * allow type inference * JobSummarySet * remove odd comment * Reflect that browser timezone may be undefined in the BaseParams * comment about optional browserTimezone * revert unecessary es archive change Co-authored-by: Elastic Machine --- x-pack/plugins/reporting/common/types.ts | 71 +++++----- .../components/buttons/report_info_button.tsx | 15 +- .../public/components/job_download_button.tsx | 3 +- .../public/components/job_failure.tsx | 4 +- .../public/components/job_success.tsx | 5 +- .../components/job_warning_formulas.tsx | 5 +- .../components/job_warning_max_size.tsx | 5 +- .../public/components/report_listing.tsx | 6 +- .../components/reporting_panel_content.tsx | 7 +- .../screen_capture_panel_content.tsx | 9 +- x-pack/plugins/reporting/public/index.ts | 21 +++ .../__snapshots__/stream_handler.test.ts.snap | 10 +- .../public/lib/reporting_api_client.ts | 50 ++----- .../public/lib/stream_handler.test.ts | 27 ++-- .../reporting/public/lib/stream_handler.ts | 21 +-- x-pack/plugins/reporting/public/plugin.tsx | 8 +- .../register_csv_reporting.tsx | 4 +- .../register_pdf_png_reporting.tsx | 6 +- .../chromium/driver/chromium_driver.ts | 4 +- .../browsers/chromium/driver_factory/index.ts | 2 +- .../common/decrypt_job_headers.test.ts | 29 ++-- .../common/decrypt_job_headers.ts | 25 +--- .../common/get_conditional_headers.test.ts | 21 +-- .../common/get_conditional_headers.ts | 15 +- .../export_types/common/get_full_urls.test.ts | 82 ++++------- .../export_types/common/get_full_urls.ts | 8 +- .../server/export_types/common/index.ts | 18 +++ .../common/omit_blocked_headers.test.ts | 17 +-- .../common/omit_blocked_headers.ts | 8 +- .../server/export_types/csv/create_job.ts | 9 +- .../server/export_types/csv/execute_job.ts | 2 +- .../export_types/csv/generate_csv/index.ts | 2 +- .../server/export_types/csv/index.ts | 6 +- .../server/export_types/csv/types.d.ts | 34 ++--- .../csv_from_savedobject/create_job.ts | 48 ++----- .../csv_from_savedobject/execute_job.ts | 23 +-- .../csv_from_savedobject/index.ts | 3 - .../lib/get_csv_job.test.ts | 2 +- .../csv_from_savedobject/lib/get_csv_job.ts | 8 +- .../lib/get_filters.test.ts | 2 +- .../csv_from_savedobject/lib/get_filters.ts | 2 +- .../csv_from_savedobject/types.d.ts | 12 +- .../export_types/png/create_job/index.ts | 5 +- .../export_types/png/execute_job/index.ts | 21 ++- .../server/export_types/png/index.ts | 2 - .../export_types/png/lib/generate_png.ts | 4 +- .../server/export_types/png/types.d.ts | 18 ++- .../printable_pdf/create_job/index.ts | 5 +- .../printable_pdf/execute_job/index.ts | 20 ++- .../export_types/printable_pdf/index.ts | 2 - .../printable_pdf/lib/generate_pdf.ts | 8 +- .../printable_pdf/lib/get_custom_logo.test.ts | 7 +- .../printable_pdf/lib/get_custom_logo.ts | 2 +- .../export_types/printable_pdf/types.d.ts | 21 ++- .../reporting/server/lib/check_license.ts | 8 +- .../reporting/server/lib/create_queue.ts | 6 +- .../reporting/server/lib/create_worker.ts | 18 +-- .../reporting/server/lib/enqueue_job.ts | 29 +++- .../server/lib/export_types_registry.ts | 51 +++---- .../server/lib/layouts/create_layout.ts | 5 +- .../reporting/server/lib/layouts/index.ts | 4 +- .../server/lib/layouts/preserve_layout.ts | 3 +- .../server/lib/layouts/print_layout.ts | 10 +- .../server/lib/screenshots/get_time_range.ts | 2 +- .../reporting/server/lib/screenshots/index.ts | 4 +- .../server/lib/screenshots/observable.test.ts | 2 +- .../server/lib/screenshots/open_url.ts | 5 +- .../reporting/server/lib/store/index.ts | 2 +- .../reporting/server/lib/store/report.test.ts | 100 ++++++------- .../reporting/server/lib/store/report.ts | 96 +++++++------ .../reporting/server/lib/store/store.test.ts | 133 +++++++++--------- .../reporting/server/lib/store/store.ts | 74 ++-------- .../reporting/server/lib/tasks/index.ts | 32 +++++ .../server/routes/diagnostic/browser.ts | 2 +- .../server/routes/diagnostic/config.ts | 2 +- .../server/routes/diagnostic/index.ts | 6 + .../server/routes/diagnostic/screenshot.ts | 7 +- .../generate_from_savedobject_immediate.ts | 24 ++-- .../server/routes/generation.test.ts | 20 ++- .../plugins/reporting/server/routes/index.ts | 7 + .../reporting/server/routes/jobs.test.ts | 5 +- .../plugins/reporting/server/routes/jobs.ts | 2 +- .../server/routes/lib/get_document_payload.ts | 33 +++-- .../routes/lib/get_job_params_from_request.ts | 6 +- .../reporting/server/routes/lib/jobs_query.ts | 5 +- .../reporting/server/routes/types.d.ts | 4 +- x-pack/plugins/reporting/server/types.ts | 110 +++------------ 87 files changed, 698 insertions(+), 893 deletions(-) create mode 100644 x-pack/plugins/reporting/server/lib/tasks/index.ts diff --git a/x-pack/plugins/reporting/common/types.ts b/x-pack/plugins/reporting/common/types.ts index 18b0ac2a72802..24c126bfe0571 100644 --- a/x-pack/plugins/reporting/common/types.ts +++ b/x-pack/plugins/reporting/common/types.ts @@ -7,7 +7,12 @@ // eslint-disable-next-line @kbn/eslint/no-restricted-paths export { ReportingConfigType } from '../server/config'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { LayoutInstance } from '../server/lib/layouts'; +import { LayoutParams } from '../server/lib/layouts'; +export { LayoutParams }; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +export { ReportDocument, ReportSource } from '../server/lib/store/report'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +export { BaseParams } from '../server/types'; export type JobId = string; export type JobStatus = @@ -17,45 +22,43 @@ export type JobStatus = | 'processing' | 'failed'; -export interface SourceJob { - _id: JobId; - _source: { - status: JobStatus; - output: { - max_size_reached: boolean; - csv_contains_formulas: boolean; - }; - payload: { - type: string; - title: string; - }; - }; -} - export interface JobContent { content: string; } -export interface JobSummary { - id: JobId; - status: JobStatus; - title: string; - type: string; - maxSizeReached: boolean; - csvContainsFormulas: boolean; -} - -export interface JobStatusBuckets { - completed: JobSummary[]; - failed: JobSummary[]; +export interface ReportApiJSON { + id: string; + index: string; + kibana_name: string; + kibana_id: string; + browser_type: string | undefined; + created_at: string; + priority?: number; + jobtype: string; + created_by: string | false; + timeout?: number; + output?: { + content_type: string; + size: number; + warnings?: string[]; + }; + process_expiration?: string; + completed_at: string | undefined; + payload: { + layout?: LayoutParams; + title: string; + browserTimezone?: string; + }; + meta: { + layout?: string; + objectType: string; + }; + max_attempts: number; + started_at: string | undefined; + attempts: number; + status: string; } -type DownloadLink = string; -export type DownloadReportFn = (jobId: JobId) => DownloadLink; - -type ManagementLink = string; -export type ManagementLinkFn = () => ManagementLink; - export interface PollerOptions { functionToPoll: () => Promise; pollFrequencyInMillis: number; diff --git a/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx index 941baa5af6776..068cb7d44b0a1 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_info_button.tsx @@ -15,10 +15,11 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import React, { Component, Fragment } from 'react'; import { get } from 'lodash'; +import React, { Component, Fragment } from 'react'; +import { ReportApiJSON } from '../../../common/types'; import { USES_HEADLESS_JOB_TYPES } from '../../../constants'; -import { JobInfo, ReportingAPIClient } from '../../lib/reporting_api_client'; +import { ReportingAPIClient } from '../../lib/reporting_api_client'; interface Props { jobId: string; @@ -29,14 +30,14 @@ interface State { isLoading: boolean; isFlyoutVisible: boolean; calloutTitle: string; - info: JobInfo | null; + info: ReportApiJSON | null; error: Error | null; } const NA = 'n/a'; const UNKNOWN = 'unknown'; -const getDimensions = (info: JobInfo): string => { +const getDimensions = (info: ReportApiJSON): string => { const defaultDimensions = { width: null, height: null }; const { width, height } = get(info, 'payload.layout.dimensions', defaultDimensions); if (width && height) { @@ -121,10 +122,6 @@ export class ReportInfoButton extends Component { title: 'Title', description: get(info, 'payload.title') || NA, }, - { - title: 'Type', - description: get(info, 'payload.type') || NA, - }, { title: 'Layout', description: get(info, 'meta.layout') || NA, @@ -263,7 +260,7 @@ export class ReportInfoButton extends Component { private loadInfo = async () => { this.setState({ isLoading: true }); try { - const info: JobInfo = await this.props.apiClient.getInfo(this.props.jobId); + const info: ReportApiJSON = await this.props.apiClient.getInfo(this.props.jobId); if (this.mounted) { this.setState({ isLoading: false, info }); } diff --git a/x-pack/plugins/reporting/public/components/job_download_button.tsx b/x-pack/plugins/reporting/public/components/job_download_button.tsx index 7dff2cafa047b..8cf3ce8644add 100644 --- a/x-pack/plugins/reporting/public/components/job_download_button.tsx +++ b/x-pack/plugins/reporting/public/components/job_download_button.tsx @@ -7,7 +7,8 @@ import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { JobId, JobSummary } from '../../common/types'; +import { JobSummary } from '../'; +import { JobId } from '../../common/types'; interface Props { getUrl: (jobId: JobId) => string; diff --git a/x-pack/plugins/reporting/public/components/job_failure.tsx b/x-pack/plugins/reporting/public/components/job_failure.tsx index 0da67ea367437..8d8f32f692343 100644 --- a/x-pack/plugins/reporting/public/components/job_failure.tsx +++ b/x-pack/plugins/reporting/public/components/job_failure.tsx @@ -9,8 +9,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; +import { JobSummary, ManagementLinkFn } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { JobSummary, ManagementLinkFn } from '../../common/types'; export const getFailureToast = ( errorText: string, @@ -22,7 +22,7 @@ export const getFailureToast = ( ), text: toMountPoint( diff --git a/x-pack/plugins/reporting/public/components/job_success.tsx b/x-pack/plugins/reporting/public/components/job_success.tsx index 7f33321ee3645..05cf2c4c5784a 100644 --- a/x-pack/plugins/reporting/public/components/job_success.tsx +++ b/x-pack/plugins/reporting/public/components/job_success.tsx @@ -7,8 +7,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; +import { JobSummary } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { JobId, JobSummary } from '../../common/types'; +import { JobId } from '../../common/types'; import { DownloadButton } from './job_download_button'; import { ReportLink } from './report_link'; @@ -21,7 +22,7 @@ export const getSuccessToast = ( ), color: 'success', diff --git a/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx b/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx index e2afae1feaa01..8cccc94e98dcd 100644 --- a/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx +++ b/x-pack/plugins/reporting/public/components/job_warning_formulas.tsx @@ -7,8 +7,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; +import { JobSummary } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { JobId, JobSummary } from '../../common/types'; +import { JobId } from '../../common/types'; import { DownloadButton } from './job_download_button'; import { ReportLink } from './report_link'; @@ -21,7 +22,7 @@ export const getWarningFormulasToast = ( ), text: toMountPoint( diff --git a/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx b/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx index 6c0d6118dfff2..c350eef0e5a54 100644 --- a/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx +++ b/x-pack/plugins/reporting/public/components/job_warning_max_size.tsx @@ -7,8 +7,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { ToastInput } from 'src/core/public'; +import { JobSummary } from '../'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { JobId, JobSummary } from '../../common/types'; +import { JobId } from '../../common/types'; import { DownloadButton } from './job_download_button'; import { ReportLink } from './report_link'; @@ -21,7 +22,7 @@ export const getWarningMaxSizeToast = ( ), text: toMountPoint( diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index f326d365351f2..cea402d6a98f2 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -41,17 +41,17 @@ export interface Job { type: string; object_type: string; object_title: string; - created_by?: string; + created_by?: string | false; created_at: string; started_at?: string; completed_at?: string; status: string; statusLabel: string; - max_size_reached: boolean; + max_size_reached?: boolean; attempts: number; max_attempts: number; csv_contains_formulas: boolean; - warnings: string[]; + warnings?: string[]; } export interface Props { diff --git a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx index eddf151167be8..22b97f45db186 100644 --- a/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/reporting_panel_content.tsx @@ -7,10 +7,11 @@ import { EuiButton, EuiCopy, EuiForm, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component, ReactElement } from 'react'; -import url from 'url'; import { ToastsSetup } from 'src/core/public'; -import { ReportingAPIClient } from '../lib/reporting_api_client'; +import url from 'url'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; +import { BaseParams } from '../../common/types'; +import { ReportingAPIClient } from '../lib/reporting_api_client'; interface Props { apiClient: ReportingAPIClient; @@ -19,7 +20,7 @@ interface Props { layoutId: string | undefined; objectId?: string; objectType: string; - getJobParams: () => any; + getJobParams: () => BaseParams; options?: ReactElement; isDirty: boolean; onClose: () => void; diff --git a/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx index 9fb74a70ff1ac..4a62ab2b76508 100644 --- a/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx +++ b/x-pack/plugins/reporting/public/components/screen_capture_panel_content.tsx @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiSpacer, EuiSwitch } from '@elastic/eui'; +import { EuiSpacer, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; import { ToastsSetup } from 'src/core/public'; -import { ReportingPanelContent } from './reporting_panel_content'; +import { BaseParams } from '../../common/types'; import { ReportingAPIClient } from '../lib/reporting_api_client'; +import { ReportingPanelContent } from './reporting_panel_content'; interface Props { apiClient: ReportingAPIClient; @@ -17,7 +18,7 @@ interface Props { reportType: string; objectId?: string; objectType: string; - getJobParams: () => any; + getJobParams: () => BaseParams; isDirty: boolean; onClose: () => void; } @@ -83,7 +84,7 @@ export class ScreenCapturePanelContent extends Component { ); }; - private handlePrintLayoutChange = (evt: any) => { + private handlePrintLayoutChange = (evt: EuiSwitchEvent) => { this.setState({ usePrintLayout: evt.target.checked }); }; diff --git a/x-pack/plugins/reporting/public/index.ts b/x-pack/plugins/reporting/public/index.ts index 185367a85bdc0..251fd14ee4d57 100644 --- a/x-pack/plugins/reporting/public/index.ts +++ b/x-pack/plugins/reporting/public/index.ts @@ -7,6 +7,7 @@ import { PluginInitializerContext } from 'src/core/public'; import { ReportingPublicPlugin } from './plugin'; import * as jobCompletionNotifications from './lib/job_completion_notifications'; +import { JobId, JobStatus } from '../common/types'; export function plugin(initializerContext: PluginInitializerContext) { return new ReportingPublicPlugin(initializerContext); @@ -14,3 +15,23 @@ export function plugin(initializerContext: PluginInitializerContext) { export { ReportingPublicPlugin as Plugin }; export { jobCompletionNotifications }; + +export interface JobSummary { + id: JobId; + status: JobStatus; + title: string; + jobtype: string; + maxSizeReached?: boolean; + csvContainsFormulas?: boolean; +} + +export interface JobSummarySet { + completed: JobSummary[]; + failed: JobSummary[]; +} + +type DownloadLink = string; +export type DownloadReportFn = (jobId: JobId) => DownloadLink; + +type ManagementLink = string; +export type ManagementLinkFn = () => ManagementLink; diff --git a/x-pack/plugins/reporting/public/lib/__snapshots__/stream_handler.test.ts.snap b/x-pack/plugins/reporting/public/lib/__snapshots__/stream_handler.test.ts.snap index 6b95a00ea0009..f1d9d747a7236 100644 --- a/x-pack/plugins/reporting/public/lib/__snapshots__/stream_handler.test.ts.snap +++ b/x-pack/plugins/reporting/public/lib/__snapshots__/stream_handler.test.ts.snap @@ -6,20 +6,20 @@ Object { Object { "csvContainsFormulas": false, "id": "job-source-mock1", + "jobtype": undefined, "maxSizeReached": false, "status": "completed", "title": "specimen", - "type": "spectacular", }, ], "failed": Array [ Object { "csvContainsFormulas": false, "id": "job-source-mock2", + "jobtype": undefined, "maxSizeReached": false, "status": "failed", "title": "specimen", - "type": "spectacular", }, ], } @@ -49,9 +49,9 @@ Array [ Object { "csvContainsFormulas": true, "id": "yas3", + "jobtype": "yas", "status": "completed", "title": "Yas", - "type": "yas", } } /> @@ -149,10 +149,10 @@ Array [ job={ Object { "id": "yas2", + "jobtype": "yas", "maxSizeReached": true, "status": "completed", "title": "Yas", - "type": "yas", } } /> @@ -191,9 +191,9 @@ Array [ job={ Object { "id": "yas1", + "jobtype": "yas", "status": "completed", "title": "Yas", - "type": "yas", } } /> diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts index 2f813bd811c6c..2853caaaaa1b5 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts @@ -7,10 +7,11 @@ import { stringify } from 'query-string'; import rison from 'rison-node'; import { HttpSetup } from 'src/core/public'; -import { JobId, SourceJob } from '../../common/types'; +import { DownloadReportFn, ManagementLinkFn } from '../'; +import { JobId, ReportApiJSON, ReportDocument, ReportSource } from '../../common/types'; import { - API_BASE_URL, API_BASE_GENERATE, + API_BASE_URL, API_LIST_URL, REPORTING_MANAGEMENT_HOME, } from '../../constants'; @@ -18,7 +19,7 @@ import { add } from './job_completion_notifications'; export interface JobQueueEntry { _id: string; - _source: any; + _source: ReportSource; } export interface JobContent { @@ -26,40 +27,6 @@ export interface JobContent { content_type: boolean; } -export interface JobInfo { - kibana_name: string; - kibana_id: string; - browser_type: string; - created_at: string; - priority: number; - jobtype: string; - created_by: string; - timeout: number; - output: { - content_type: string; - size: number; - warnings: string[]; - }; - process_expiration: string; - completed_at: string; - payload: { - layout: { id: string; dimensions: { width: number; height: number } }; - objects: Array<{ relativeUrl: string }>; - type: string; - title: string; - forceNow: string; - browserTimezone: string; - }; - meta: { - layout: string; - objectType: string; - }; - max_attempts: number; - started_at: string; - attempts: number; - status: string; -} - interface JobParams { [paramName: string]: any; } @@ -121,13 +88,13 @@ export class ReportingAPIClient { }); } - public getInfo(jobId: string): Promise { + public getInfo(jobId: string): Promise { return this.http.get(`${API_LIST_URL}/info/${jobId}`, { asSystemRequest: true, }); } - public findForJobIds = (jobIds: JobId[]): Promise => { + public findForJobIds = (jobIds: JobId[]): Promise => { return this.http.fetch(`${API_LIST_URL}/list`, { query: { page: 0, ids: jobIds.join(',') }, method: 'GET', @@ -159,9 +126,10 @@ export class ReportingAPIClient { return resp; }; - public getManagementLink = () => this.http.basePath.prepend(REPORTING_MANAGEMENT_HOME); + public getManagementLink: ManagementLinkFn = () => + this.http.basePath.prepend(REPORTING_MANAGEMENT_HOME); - public getDownloadLink = (jobId: JobId) => + public getDownloadLink: DownloadReportFn = (jobId: JobId) => this.http.basePath.prepend(`${API_LIST_URL}/download/${jobId}`); /* diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts index 998f0711b1355..f91517e4397f9 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts @@ -6,7 +6,8 @@ import sinon, { stub } from 'sinon'; import { NotificationsStart } from 'src/core/public'; -import { JobSummary, SourceJob } from '../../common/types'; +import { JobSummary } from '../'; +import { ReportDocument } from '../../common/types'; import { ReportingAPIClient } from './reporting_api_client'; import { ReportingNotifierStreamHandler } from './stream_handler'; @@ -23,7 +24,7 @@ const mockJobsFound = [ _source: { status: 'completed', output: { max_size_reached: false, csv_contains_formulas: false }, - payload: { type: 'spectacular', title: 'specimen' }, + payload: { title: 'specimen' }, }, }, { @@ -31,7 +32,7 @@ const mockJobsFound = [ _source: { status: 'failed', output: { max_size_reached: false, csv_contains_formulas: false }, - payload: { type: 'spectacular', title: 'specimen' }, + payload: { title: 'specimen' }, }, }, { @@ -39,14 +40,14 @@ const mockJobsFound = [ _source: { status: 'pending', output: { max_size_reached: false, csv_contains_formulas: false }, - payload: { type: 'spectacular', title: 'specimen' }, + payload: { title: 'specimen' }, }, }, ]; const jobQueueClientMock: ReportingAPIClient = { findForJobIds: async (jobIds: string[]) => { - return mockJobsFound as SourceJob[]; + return mockJobsFound as ReportDocument[]; }, getContent: (): Promise => { return Promise.resolve({ content: 'this is the completed report data' }); @@ -109,7 +110,7 @@ describe('stream handler', () => { { id: 'yas1', title: 'Yas', - type: 'yas', + jobtype: 'yas', status: 'completed', } as JobSummary, ], @@ -130,7 +131,7 @@ describe('stream handler', () => { { id: 'yas2', title: 'Yas', - type: 'yas', + jobtype: 'yas', status: 'completed', maxSizeReached: true, } as JobSummary, @@ -152,7 +153,7 @@ describe('stream handler', () => { { id: 'yas3', title: 'Yas', - type: 'yas', + jobtype: 'yas', status: 'completed', csvContainsFormulas: true, } as JobSummary, @@ -175,7 +176,7 @@ describe('stream handler', () => { { id: 'yas7', title: 'Yas 7', - type: 'yas', + jobtype: 'yas', status: 'failed', } as JobSummary, ], @@ -195,20 +196,20 @@ describe('stream handler', () => { { id: 'yas8', title: 'Yas 8', - type: 'yas', + jobtype: 'yas', status: 'completed', } as JobSummary, { id: 'yas9', title: 'Yas 9', - type: 'yas', + jobtype: 'yas', status: 'completed', csvContainsFormulas: true, } as JobSummary, { id: 'yas10', title: 'Yas 10', - type: 'yas', + jobtype: 'yas', status: 'completed', maxSizeReached: true, } as JobSummary, @@ -217,7 +218,7 @@ describe('stream handler', () => { { id: 'yas13', title: 'Yas 13', - type: 'yas', + jobtype: 'yas', status: 'failed', } as JobSummary, ], diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts index 80ba02e17d56d..d97c0a7a2d11e 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts @@ -8,7 +8,8 @@ import { i18n } from '@kbn/i18n'; import * as Rx from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { NotificationsSetup } from 'src/core/public'; -import { JobId, JobStatusBuckets, JobSummary, SourceJob } from '../../common/types'; +import { JobSummarySet, JobSummary } from '../'; +import { JobId, ReportDocument } from '../../common/types'; import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JOB_STATUS_COMPLETED, @@ -28,14 +29,14 @@ function updateStored(jobIds: JobId[]): void { sessionStorage.setItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JSON.stringify(jobIds)); } -function summarizeJob(src: SourceJob): JobSummary { +function getReportStatus(src: ReportDocument): JobSummary { return { id: src._id, status: src._source.status, title: src._source.payload.title, - type: src._source.payload.type, - maxSizeReached: src._source.output.max_size_reached, - csvContainsFormulas: src._source.output.csv_contains_formulas, + jobtype: src._source.jobtype, + maxSizeReached: src._source.output?.max_size_reached, + csvContainsFormulas: src._source.output?.csv_contains_formulas, }; } @@ -48,7 +49,7 @@ export class ReportingNotifierStreamHandler { public showNotifications({ completed: completedJobs, failed: failedJobs, - }: JobStatusBuckets): Rx.Observable { + }: JobSummarySet): Rx.Observable { const showNotificationsAsync = async () => { // notifications with download link for (const job of completedJobs) { @@ -92,9 +93,9 @@ export class ReportingNotifierStreamHandler { * An observable that finds jobs that are known to be "processing" (stored in * session storage) but have non-processing job status on the server */ - public findChangedStatusJobs(storedJobs: JobId[]): Rx.Observable { + public findChangedStatusJobs(storedJobs: JobId[]): Rx.Observable { return Rx.from(this.apiClient.findForJobIds(storedJobs)).pipe( - map((jobs: SourceJob[]) => { + map((jobs: ReportDocument[]) => { const completedJobs: JobSummary[] = []; const failedJobs: JobSummary[] = []; const pending: JobId[] = []; @@ -107,9 +108,9 @@ export class ReportingNotifierStreamHandler { } = job; if (storedJobs.includes(jobId)) { if (jobStatus === JOB_STATUS_COMPLETED || jobStatus === JOB_STATUS_WARNINGS) { - completedJobs.push(summarizeJob(job)); + completedJobs.push(getReportStatus(job)); } else if (jobStatus === JOB_STATUS_FAILED) { - failedJobs.push(summarizeJob(job)); + failedJobs.push(getReportStatus(job)); } else { pending.push(jobId); } diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.tsx index a134377e194b8..cc5964f737988 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.tsx @@ -27,8 +27,9 @@ import { ManagementSetup } from '../../../../src/plugins/management/public'; import { SharePluginSetup } from '../../../../src/plugins/share/public'; import { LicensingPluginSetup } from '../../licensing/public'; import { durationToNumber } from '../common/schema_utils'; -import { JobId, JobStatusBuckets, ReportingConfigType } from '../common/types'; +import { JobId, ReportingConfigType } from '../common/types'; import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../constants'; +import { JobSummarySet } from './'; import { getGeneralErrorToast } from './components'; import { ReportListing } from './components/report_listing'; import { ReportingAPIClient } from './lib/reporting_api_client'; @@ -46,10 +47,7 @@ function getStored(): JobId[] { return sessionValue ? JSON.parse(sessionValue) : []; } -function handleError( - notifications: NotificationsSetup, - err: Error -): Rx.Observable { +function handleError(notifications: NotificationsSetup, err: Error): Rx.Observable { notifications.toasts.addDanger( getGeneralErrorToast( i18n.translate('xpack.reporting.publicNotifier.pollingErrorMessage', { diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index 4ad35fd768825..451d907199c4c 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { IUiSettingsClient, ToastsSetup } from 'src/core/public'; import { ShareContext } from '../../../../../src/plugins/share/public'; import { LicensingPluginSetup } from '../../../licensing/public'; -import { JobParamsDiscoverCsv, SearchRequest } from '../../server/export_types/csv/types'; +import { JobParamsCSV, SearchRequest } from '../../server/export_types/csv/types'; import { ReportingPanelContent } from '../components/reporting_panel_content'; import { checkLicense } from '../lib/license_check'; import { ReportingAPIClient } from '../lib/reporting_api_client'; @@ -59,7 +59,7 @@ export const csvReportingProvider = ({ return []; } - const jobParams: JobParamsDiscoverCsv = { + const jobParams: JobParamsCSV = { browserTimezone, objectType, title: sharingData.title as string, diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx index e10d04ea5fc6b..2dab66187bb25 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { IUiSettingsClient, ToastsSetup } from 'src/core/public'; import { ShareContext } from '../../../../../src/plugins/share/public'; import { LicensingPluginSetup } from '../../../licensing/public'; -import { LayoutInstance } from '../../common/types'; +import { LayoutParams } from '../../common/types'; import { JobParamsPNG } from '../../server/export_types/png/types'; import { JobParamsPDF } from '../../server/export_types/printable_pdf/types'; import { ScreenCapturePanelContent } from '../components/screen_capture_panel_content'; @@ -80,7 +80,7 @@ export const reportingPDFPNGProvider = ({ objectType, browserTimezone, relativeUrls: [relativeUrl], // multi URL for PDF - layout: sharingData.layout as LayoutInstance, + layout: sharingData.layout as LayoutParams, title: sharingData.title as string, }; }; @@ -96,7 +96,7 @@ export const reportingPDFPNGProvider = ({ objectType, browserTimezone, relativeUrl, // single URL for PNG - layout: sharingData.layout as LayoutInstance, + layout: sharingData.layout as LayoutParams, title: sharingData.title as string, }; }; diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index 0a76c7fcfd3b2..04ab572a53dbc 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -10,10 +10,10 @@ import open from 'opn'; import { ElementHandle, EvaluateFn, Page, Response, SerializableOrJSHandle } from 'puppeteer'; import { parse as parseUrl } from 'url'; import { getDisallowedOutgoingUrlError } from '../'; +import { ConditionalHeaders, ConditionalHeadersConditions } from '../../../export_types/common'; import { LevelLogger } from '../../../lib'; import { ViewZoomWidthHeight } from '../../../lib/layouts/layout'; import { ElementPosition } from '../../../lib/screenshots'; -import { ConditionalHeaders } from '../../../types'; import { allowRequest, NetworkPolicy } from '../../network_policy'; export interface ChromiumDriverOptions { @@ -34,8 +34,6 @@ interface EvaluateMetaOpts { context: string; } -type ConditionalHeadersConditions = ConditionalHeaders['conditions']; - interface InterceptedRequest { requestId: string; request: { diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 6897f07c45e2b..efef323612322 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -64,7 +64,7 @@ export class HeadlessChromiumDriverFactory { * Return an observable to objects which will drive screenshot capture for a page */ createPage( - { viewport, browserTimezone }: { viewport: ViewportConfig; browserTimezone: string }, + { viewport, browserTimezone }: { viewport: ViewportConfig; browserTimezone?: string }, pLogger: LevelLogger ): Rx.Observable<{ driver: HeadlessChromiumDriver; exit$: Rx.Observable }> { return Rx.Observable.create(async (observer: InnerSubscriber) => { diff --git a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts index 908817a2ccf81..db1e622df4e21 100644 --- a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cryptoFactory, LevelLogger } from '../../lib'; +import { cryptoFactory } from '../../lib'; +import { createMockLevelLogger } from '../../test_helpers'; import { decryptJobHeaders } from './'; +const logger = createMockLevelLogger(); + const encryptHeaders = async (encryptionKey: string, headers: Record) => { const crypto = cryptoFactory(encryptionKey); return await crypto.encrypt(headers); @@ -15,15 +18,11 @@ const encryptHeaders = async (encryptionKey: string, headers: Record { test(`fails if it can't decrypt headers`, async () => { const getDecryptedHeaders = () => - decryptJobHeaders({ - encryptionKey: 'abcsecretsauce', - job: { - headers: 'Q53+9A+zf+Xe+ceR/uB/aR/Sw/8e+M+qR+WiG+8z+EY+mo+HiU/zQL+Xn', - }, - logger: ({ - error: jest.fn(), - } as unknown) as LevelLogger, - }); + decryptJobHeaders( + 'abcsecretsauce', + 'Q53+9A+zf+Xe+ceR/uB/aR/Sw/8e+M+qR+WiG+8z+EY+mo+HiU/zQL+Xn', + logger + ); await expect(getDecryptedHeaders()).rejects.toMatchInlineSnapshot( `[Error: Failed to decrypt report job data. Please ensure that xpack.reporting.encryptionKey is set and re-generate this report. Error: Invalid IV length]` ); @@ -36,15 +35,7 @@ describe('headers', () => { }; const encryptedHeaders = await encryptHeaders('abcsecretsauce', headers); - const decryptedHeaders = await decryptJobHeaders({ - encryptionKey: 'abcsecretsauce', - job: { - title: 'cool-job-bro', - type: 'csv', - headers: encryptedHeaders, - }, - logger: {} as LevelLogger, - }); + const decryptedHeaders = await decryptJobHeaders('abcsecretsauce', encryptedHeaders, logger); expect(decryptedHeaders).toEqual(headers); }); }); diff --git a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts index 4f0088467dd68..131a7936e3463 100644 --- a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.ts @@ -7,24 +7,13 @@ import { i18n } from '@kbn/i18n'; import { cryptoFactory, LevelLogger } from '../../lib'; -interface HasEncryptedHeaders { - headers?: string; -} - -export const decryptJobHeaders = async < - JobParamsType, - TaskPayloadType extends HasEncryptedHeaders ->({ - encryptionKey, - job, - logger, -}: { - encryptionKey?: string; - job: TaskPayloadType; - logger: LevelLogger; -}): Promise> => { +export const decryptJobHeaders = async ( + encryptionKey: string | undefined, + headers: string, + logger: LevelLogger +): Promise> => { try { - if (typeof job.headers !== 'string') { + if (typeof headers !== 'string') { throw new Error( i18n.translate('xpack.reporting.exportTypes.common.missingJobHeadersErrorMessage', { defaultMessage: 'Job headers are missing', @@ -32,7 +21,7 @@ export const decryptJobHeaders = async < ); } const crypto = cryptoFactory(encryptionKey); - const decryptedHeaders = (await crypto.decrypt(job.headers)) as Record; + const decryptedHeaders = (await crypto.decrypt(headers)) as Record; return decryptedHeaders; } catch (err) { logger.error(err); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts index 794ea9febb5c0..b1d6f6fdf79c1 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts @@ -6,7 +6,6 @@ import { ReportingConfig } from '../../'; import { createMockConfig, createMockConfigSchema } from '../../test_helpers'; -import { BasePayload } from '../../types'; import { getConditionalHeaders } from './'; let mockConfig: ReportingConfig; @@ -24,11 +23,7 @@ describe('conditions', () => { baz: 'quix', }; - const conditionalHeaders = getConditionalHeaders({ - job: {} as BasePayload, - filteredHeaders: permittedHeaders, - config: mockConfig, - }); + const conditionalHeaders = getConditionalHeaders(mockConfig, permittedHeaders); expect(conditionalHeaders.conditions.hostname).toEqual( mockConfig.get('kibanaServer', 'hostname') @@ -49,19 +44,7 @@ describe('config formatting', () => { const mockSchema = createMockConfigSchema(reportingConfig); mockConfig = createMockConfig(mockSchema); - const conditionalHeaders = getConditionalHeaders({ - job: { - title: 'cool-job-bro', - type: 'csv', - jobParams: { - savedObjectId: 'abc-123', - isImmediate: false, - savedObjectType: 'search', - }, - }, - filteredHeaders: {}, - config: mockConfig, - }); + const conditionalHeaders = getConditionalHeaders(mockConfig, {}); expect(conditionalHeaders.conditions.hostname).toEqual('great-hostname'); }); }); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.ts b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.ts index ce83323914eb8..d167ac21635b1 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.ts @@ -5,17 +5,12 @@ */ import { ReportingConfig } from '../../'; -import { ConditionalHeaders } from '../../types'; +import { ConditionalHeaders } from './'; -export const getConditionalHeaders = ({ - config, - job, - filteredHeaders, -}: { - config: ReportingConfig; - job: TaskPayloadType; - filteredHeaders: Record; -}) => { +export const getConditionalHeaders = ( + config: ReportingConfig, + filteredHeaders: Record +) => { const { kbnConfig } = config; const [hostname, port, basePath, protocol] = [ config.get('kibanaServer', 'hostname'), diff --git a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts index fae66b26a83e0..6a4e21b08996e 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts @@ -10,11 +10,6 @@ import { TaskPayloadPNG } from '../png/types'; import { TaskPayloadPDF } from '../printable_pdf/types'; import { getFullUrls } from './get_full_urls'; -interface FullUrlsOpts { - job: TaskPayloadPNG & TaskPayloadPDF; - config: ReportingConfig; -} - let mockConfig: ReportingConfig; beforeEach(() => { @@ -30,7 +25,7 @@ beforeEach(() => { const getMockJob = (base: object) => base as TaskPayloadPNG & TaskPayloadPDF; test(`fails if no URL is passed`, async () => { - const fn = () => getFullUrls({ job: getMockJob({}), config: mockConfig } as FullUrlsOpts); + const fn = () => getFullUrls(mockConfig, getMockJob({})); expect(fn).toThrowErrorMatchingInlineSnapshot( `"No valid URL fields found in Job Params! Expected \`job.relativeUrl: string\` or \`job.relativeUrls: string[]\`"` ); @@ -39,11 +34,7 @@ test(`fails if no URL is passed`, async () => { test(`fails if URLs are file-protocols for PNGs`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; const relativeUrl = 'file://etc/passwd/#/something'; - const fn = () => - getFullUrls({ - job: getMockJob({ relativeUrl, forceNow }), - config: mockConfig, - } as FullUrlsOpts); + const fn = () => getFullUrls(mockConfig, getMockJob({ relativeUrl, forceNow })); expect(fn).toThrowErrorMatchingInlineSnapshot( `"Found invalid URL(s), all URLs must be relative: file://etc/passwd/#/something"` ); @@ -53,11 +44,7 @@ test(`fails if URLs are absolute for PNGs`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; const relativeUrl = 'http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something'; - const fn = () => - getFullUrls({ - job: getMockJob({ relativeUrl, forceNow }), - config: mockConfig, - } as FullUrlsOpts); + const fn = () => getFullUrls(mockConfig, getMockJob({ relativeUrl, forceNow })); expect(fn).toThrowErrorMatchingInlineSnapshot( `"Found invalid URL(s), all URLs must be relative: http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something"` ); @@ -67,13 +54,13 @@ test(`fails if URLs are file-protocols for PDF`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; const relativeUrl = 'file://etc/passwd/#/something'; const fn = () => - getFullUrls({ - job: getMockJob({ + getFullUrls( + mockConfig, + getMockJob({ relativeUrls: [relativeUrl], forceNow, - }), - config: mockConfig, - } as FullUrlsOpts); + }) + ); expect(fn).toThrowErrorMatchingInlineSnapshot( `"Found invalid URL(s), all URLs must be relative: file://etc/passwd/#/something"` ); @@ -84,13 +71,13 @@ test(`fails if URLs are absolute for PDF`, async () => { const relativeUrl = 'http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something'; const fn = () => - getFullUrls({ - job: getMockJob({ + getFullUrls( + mockConfig, + getMockJob({ relativeUrls: [relativeUrl], forceNow, - }), - config: mockConfig, - } as FullUrlsOpts); + }) + ); expect(fn).toThrowErrorMatchingInlineSnapshot( `"Found invalid URL(s), all URLs must be relative: http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something"` ); @@ -104,22 +91,14 @@ test(`fails if any URLs are absolute or file's for PDF`, async () => { 'file://etc/passwd/#/something', ]; - const fn = () => - getFullUrls({ - job: getMockJob({ relativeUrls, forceNow }), - config: mockConfig, - } as FullUrlsOpts); + const fn = () => getFullUrls(mockConfig, getMockJob({ relativeUrls, forceNow })); expect(fn).toThrowErrorMatchingInlineSnapshot( `"Found invalid URL(s), all URLs must be relative: http://169.254.169.254/latest/meta-data/iam/security-credentials/profileName/#/something file://etc/passwd/#/something"` ); }); test(`fails if URL does not route to a visualization`, async () => { - const fn = () => - getFullUrls({ - job: getMockJob({ relativeUrl: '/app/phoney' }), - config: mockConfig, - } as FullUrlsOpts); + const fn = () => getFullUrls(mockConfig, getMockJob({ relativeUrl: '/app/phoney' })); expect(fn).toThrowErrorMatchingInlineSnapshot( `"No valid hash in the URL! A hash is expected for the application to route to the intended visualization."` ); @@ -127,10 +106,10 @@ test(`fails if URL does not route to a visualization`, async () => { test(`adds forceNow to hash's query, if it exists`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; - const urls = await getFullUrls({ - job: getMockJob({ relativeUrl: '/app/kibana#/something', forceNow }), - config: mockConfig, - } as FullUrlsOpts); + const urls = await getFullUrls( + mockConfig, + getMockJob({ relativeUrl: '/app/kibana#/something', forceNow }) + ); expect(urls[0]).toEqual( 'http://localhost:5601/sbp/app/kibana#/something?forceNow=2000-01-01T00%3A00%3A00.000Z' @@ -140,10 +119,10 @@ test(`adds forceNow to hash's query, if it exists`, async () => { test(`appends forceNow to hash's query, if it exists`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; - const urls = await getFullUrls({ - job: getMockJob({ relativeUrl: '/app/kibana#/something?_g=something', forceNow }), - config: mockConfig, - } as FullUrlsOpts); + const urls = await getFullUrls( + mockConfig, + getMockJob({ relativeUrl: '/app/kibana#/something?_g=something', forceNow }) + ); expect(urls[0]).toEqual( 'http://localhost:5601/sbp/app/kibana#/something?_g=something&forceNow=2000-01-01T00%3A00%3A00.000Z' @@ -151,18 +130,16 @@ test(`appends forceNow to hash's query, if it exists`, async () => { }); test(`doesn't append forceNow query to url, if it doesn't exists`, async () => { - const urls = await getFullUrls({ - job: getMockJob({ relativeUrl: '/app/kibana#/something' }), - config: mockConfig, - } as FullUrlsOpts); + const urls = await getFullUrls(mockConfig, getMockJob({ relativeUrl: '/app/kibana#/something' })); expect(urls[0]).toEqual('http://localhost:5601/sbp/app/kibana#/something'); }); test(`adds forceNow to each of multiple urls`, async () => { const forceNow = '2000-01-01T00:00:00.000Z'; - const urls = await getFullUrls({ - job: getMockJob({ + const urls = await getFullUrls( + mockConfig, + getMockJob({ relativeUrls: [ '/app/kibana#/something_aaa', '/app/kibana#/something_bbb', @@ -170,9 +147,8 @@ test(`adds forceNow to each of multiple urls`, async () => { '/app/kibana#/something_ddd', ], forceNow, - }), - config: mockConfig, - } as FullUrlsOpts); + }) + ); expect(urls).toEqual([ 'http://localhost:5601/sbp/app/kibana#/something_aaa?forceNow=2000-01-01T00%3A00%3A00.000Z', diff --git a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts index f4e3a7b723c08..7621a95083bc7 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.ts @@ -23,13 +23,7 @@ function isPdfJob(job: TaskPayloadPNG | TaskPayloadPDF): job is TaskPayloadPDF { return (job as TaskPayloadPDF).relativeUrls !== undefined; } -export function getFullUrls({ - config, - job, -}: { - config: ReportingConfig; - job: TaskPayloadPDF | TaskPayloadPNG; -}) { +export function getFullUrls(config: ReportingConfig, job: TaskPayloadPDF | TaskPayloadPNG) { const [basePath, protocol, hostname, port] = [ config.kbnConfig.get('server', 'basePath'), config.get('kibanaServer', 'protocol'), diff --git a/x-pack/plugins/reporting/server/export_types/common/index.ts b/x-pack/plugins/reporting/server/export_types/common/index.ts index 80eaa52d0951b..5fa313c8a2fb7 100644 --- a/x-pack/plugins/reporting/server/export_types/common/index.ts +++ b/x-pack/plugins/reporting/server/export_types/common/index.ts @@ -9,3 +9,21 @@ export { getConditionalHeaders } from './get_conditional_headers'; export { getFullUrls } from './get_full_urls'; export { omitBlockedHeaders } from './omit_blocked_headers'; export { validateUrls } from './validate_urls'; + +export interface TimeRangeParams { + timezone: string; + min?: Date | string | number | null; + max?: Date | string | number | null; +} + +export interface ConditionalHeadersConditions { + protocol: string; + hostname: string; + port: number; + basePath: string; +} + +export interface ConditionalHeaders { + headers: Record; + conditions: ConditionalHeadersConditions; +} diff --git a/x-pack/plugins/reporting/server/export_types/common/omit_blocked_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/omit_blocked_headers.test.ts index f40651603db8f..1833c2a7c62d7 100644 --- a/x-pack/plugins/reporting/server/export_types/common/omit_blocked_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/omit_blocked_headers.test.ts @@ -24,20 +24,9 @@ test(`omits blocked headers`, async () => { trailer: 's are for trucks', }; - const filteredHeaders = await omitBlockedHeaders({ - job: { - title: 'cool-job-bro', - type: 'csv', - jobParams: { - savedObjectId: 'abc-123', - isImmediate: false, - savedObjectType: 'search', - }, - }, - decryptedHeaders: { - ...permittedHeaders, - ...blockedHeaders, - }, + const filteredHeaders = omitBlockedHeaders({ + ...permittedHeaders, + ...blockedHeaders, }); expect(filteredHeaders).toEqual(permittedHeaders); diff --git a/x-pack/plugins/reporting/server/export_types/common/omit_blocked_headers.ts b/x-pack/plugins/reporting/server/export_types/common/omit_blocked_headers.ts index 946f033b4b481..09512ae703076 100644 --- a/x-pack/plugins/reporting/server/export_types/common/omit_blocked_headers.ts +++ b/x-pack/plugins/reporting/server/export_types/common/omit_blocked_headers.ts @@ -9,13 +9,7 @@ import { KBN_SCREENSHOT_HEADER_BLOCK_LIST_STARTS_WITH_PATTERN, } from '../../../common/constants'; -export const omitBlockedHeaders = ({ - job, - decryptedHeaders, -}: { - job: TaskPayloadType; - decryptedHeaders: Record; -}) => { +export const omitBlockedHeaders = (decryptedHeaders: Record) => { const filteredHeaders: Record = omitBy( decryptedHeaders, (_value, header: string) => diff --git a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts index d768dc6f8e084..cb60b218818f0 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/create_job.ts @@ -6,10 +6,11 @@ import { cryptoFactory } from '../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../types'; -import { JobParamsDiscoverCsv } from './types'; +import { IndexPatternSavedObject, JobParamsCSV, TaskPayloadCSV } from './types'; export const createJobFnFactory: CreateJobFnFactory> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); @@ -18,10 +19,10 @@ export const createJobFnFactory: CreateJobFnFactory, - TaskPayloadCSV, + CreateJobFn, RunTaskFn > => ({ ...metadata, diff --git a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts index 214157db51cb7..78615a0e7b72c 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/types.d.ts @@ -8,16 +8,6 @@ import { BaseParams, BasePayload } from '../../types'; export type RawValue = string | object | null | undefined; -interface DocValueField { - field: string; - format: string; -} - -interface SortOptions { - order: string; - unmapped_type: string; -} - export interface IndexPatternSavedObject { title: string; timeFieldName: string; @@ -28,25 +18,23 @@ export interface IndexPatternSavedObject { }; } -export interface JobParamsDiscoverCsv extends BaseParams { - browserTimezone: string; - indexPatternId: string; - title: string; +interface BaseParamsCSV { searchRequest: SearchRequest; fields: string[]; metaFields: string[]; conflictedTypesFields: string[]; } -export interface TaskPayloadCSV extends BasePayload { - browserTimezone: string; - basePath: string; - searchRequest: any; - fields: any; - indexPatternSavedObject: any; - metaFields: any; - conflictedTypesFields: any; -} +export type JobParamsCSV = BaseParamsCSV & + BaseParams & { + indexPatternId: string; + }; + +// CSV create job method converts indexPatternID to indexPatternSavedObject +export type TaskPayloadCSV = BaseParamsCSV & + BasePayload & { + indexPatternSavedObject: IndexPatternSavedObject; + }; export interface SearchRequest { index: string; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts index 1746792981a21..c780247dd61b3 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/create_job.ts @@ -6,57 +6,40 @@ import { notFound, notImplemented } from 'boom'; import { get } from 'lodash'; -import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { RequestHandlerContext } from 'src/core/server'; import { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; -import { cryptoFactory } from '../../lib'; -import { CreateJobFnFactory, TimeRangeParams } from '../../types'; +import { CsvFromSavedObjectRequest } from '../../routes/generate_from_savedobject_immediate'; +import { CreateJobFnFactory } from '../../types'; import { JobParamsPanelCsv, + JobPayloadPanelCsv, SavedObject, SavedObjectReference, SavedObjectServiceError, - SavedSearchObjectAttributesJSON, - SearchPanel, VisObjectAttributesJSON, } from './types'; export type ImmediateCreateJobFn = ( jobParams: JobParamsPanelCsv, - headers: KibanaRequest['headers'], context: RequestHandlerContext, - req: KibanaRequest -) => Promise<{ - type: string; - title: string; - jobParams: JobParamsPanelCsv; -}>; - -interface VisData { - title: string; - visType: string; - panel: SearchPanel; -} + req: CsvFromSavedObjectRequest +) => Promise; export const createJobFnFactory: CreateJobFnFactory = function createJobFactoryFn( reporting, parentLogger ) { - const config = reporting.getConfig(); - const crypto = cryptoFactory(config.get('encryptionKey')); const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'create-job']); - return async function createJob(jobParams, headers, context, req) { + return async function createJob(jobParams, context, req) { const { savedObjectType, savedObjectId } = jobParams; - const serializedEncryptedHeaders = await crypto.encrypt(headers); - const { panel, title, visType }: VisData = await Promise.resolve() + const panel = await Promise.resolve() .then(() => context.core.savedObjects.client.get(savedObjectType, savedObjectId)) .then(async (savedObject: SavedObject) => { const { attributes, references } = savedObject; - const { - kibanaSavedObjectMeta: kibanaSavedObjectMetaJSON, - } = attributes as SavedSearchObjectAttributesJSON; - const { timerange } = req.body as { timerange: TimeRangeParams }; + const { kibanaSavedObjectMeta: kibanaSavedObjectMetaJSON } = attributes; + const { timerange } = req.body; if (!kibanaSavedObjectMetaJSON) { throw new Error('Could not parse saved object data!'); @@ -85,7 +68,7 @@ export const createJobFnFactory: CreateJobFnFactory = func throw new Error('Could not find index pattern for the saved search!'); } - const sPanel = { + return { attributes: { ...attributes, kibanaSavedObjectMeta: { searchSource }, @@ -93,8 +76,6 @@ export const createJobFnFactory: CreateJobFnFactory = func indexPatternSavedObjectId: indexPatternMeta.id, timerange, }; - - return { panel: sPanel, title: attributes.title, visType: 'search' }; }) .catch((err: Error) => { const boomErr = (err as unknown) as { isBoom: boolean }; @@ -109,11 +90,6 @@ export const createJobFnFactory: CreateJobFnFactory = func throw new Error(`Unable to create a job from saved object data! Error: ${err}`); }); - return { - headers: serializedEncryptedHeaders, - jobParams: { ...jobParams, panel, visType }, - type: visType, - title, - }; + return { ...jobParams, panel }; }; }; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts index 0ca80581fcc83..19348c0a678d7 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/execute_job.ts @@ -7,16 +7,11 @@ import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { CancellationToken } from '../../../common'; import { CONTENT_TYPE_CSV, CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; -import { BasePayload, RunTaskFnFactory, TaskRunResult } from '../../types'; +import { TaskRunResult } from '../../lib/tasks'; +import { RunTaskFnFactory } from '../../types'; import { createGenerateCsv } from '../csv/generate_csv'; import { getGenerateCsvParams } from './lib/get_csv_job'; -import { JobParamsPanelCsv, SearchPanel } from './types'; - -/* - * The run function receives the full request which provides the un-encrypted - * headers, so encrypted headers are not part of these kind of job params - */ -type ImmediateJobParams = Omit, 'headers'>; +import { JobPayloadPanelCsv } from './types'; /* * ImmediateExecuteFn receives the job doc payload because the payload was @@ -24,7 +19,7 @@ type ImmediateJobParams = Omit, 'headers'>; */ export type ImmediateExecuteFn = ( jobId: null, - job: ImmediateJobParams, + job: JobPayloadPanelCsv, context: RequestHandlerContext, req: KibanaRequest ) => Promise; @@ -36,20 +31,16 @@ export const runTaskFnFactory: RunTaskFnFactory = function e const config = reporting.getConfig(); const logger = parentLogger.clone([CSV_FROM_SAVEDOBJECT_JOB_TYPE, 'execute-job']); - return async function runTask(jobId: string | null, jobPayload, context, req) { - // There will not be a jobID for "immediate" generation. - // jobID is only for "queued" jobs - // Use the jobID as a logging tag or "immediate" - const { jobParams } = jobPayload; + return async function runTask(jobId, jobPayload, context, req) { const jobLogger = logger.clone(['immediate']); const generateCsv = createGenerateCsv(jobLogger); - const { panel, visType } = jobParams as JobParamsPanelCsv & { panel: SearchPanel }; + const { panel, visType } = jobPayload; jobLogger.debug(`Execute job generating [${visType}] csv`); const savedObjectsClient = context.core.savedObjects.client; const uiSettingsClient = await reporting.getUiSettingsServiceFactory(savedObjectsClient); - const job = await getGenerateCsvParams(jobParams, panel, savedObjectsClient, uiSettingsClient); + const job = await getGenerateCsvParams(jobPayload, panel, savedObjectsClient, uiSettingsClient); const elasticsearch = reporting.getElasticsearchService(); const { callAsCurrentUser } = elasticsearch.legacy.client.asScoped(req); diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts index 4b4cfb3f062bf..abe9fbf3e3950 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/index.ts @@ -17,7 +17,6 @@ import { ExportTypeDefinition } from '../../types'; import { createJobFnFactory, ImmediateCreateJobFn } from './create_job'; import { ImmediateExecuteFn, runTaskFnFactory } from './execute_job'; import { metadata } from './metadata'; -import { JobParamsPanelCsv } from './types'; /* * These functions are exported to share with the API route handler that @@ -27,9 +26,7 @@ export { createJobFnFactory } from './create_job'; export { runTaskFnFactory } from './execute_job'; export const getExportType = (): ExportTypeDefinition< - JobParamsPanelCsv, ImmediateCreateJobFn, - JobParamsPanelCsv, ImmediateExecuteFn > => ({ ...metadata, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts index b387245406fbb..acf749584c6cd 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.test.ts @@ -13,7 +13,7 @@ describe('Get CSV Job', () => { let mockSavedObjectsClient: any; let mockUiSettingsClient: any; beforeEach(() => { - mockJobParams = { isImmediate: true, savedObjectType: 'search', savedObjectId: '234-ididid' }; + mockJobParams = { savedObjectType: 'search', savedObjectId: '234-ididid' }; mockSearchPanel = { indexPatternSavedObjectId: '123-indexId', attributes: { diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts index 26a4b17aaf71f..1fe64a25ebcaa 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts @@ -12,7 +12,7 @@ import { IIndexPattern, Query, } from '../../../../../../../src/plugins/data/server'; -import { TimeRangeParams } from '../../../types'; +import { TimeRangeParams } from '../../common'; import { GenerateCsvParams } from '../../csv/generate_csv'; import { DocValueFields, @@ -50,11 +50,11 @@ export const getGenerateCsvParams = async ( savedObjectsClient: SavedObjectsClientContract, uiConfig: IUiSettingsClient ): Promise => { - let timerange: TimeRangeParams; + let timerange: TimeRangeParams | null; if (jobParams.post?.timerange) { timerange = jobParams.post?.timerange; } else { - timerange = panel.timerange; + timerange = panel.timerange || null; } const { indexPatternSavedObjectId } = panel; const savedSearchObjectAttr = panel.attributes as SavedSearchObjectAttributes; @@ -137,7 +137,7 @@ export const getGenerateCsvParams = async ( }; return { - browserTimezone: timerange.timezone, + browserTimezone: timerange?.timezone, indexPatternSavedObject, searchRequest, fields: includes, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.test.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.test.ts index 429b2c518cf14..75e979aa2ec01 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TimeRangeParams } from '../../../types'; +import { TimeRangeParams } from '../../common'; import { QueryFilter, SavedSearchObjectAttributes, SearchSourceFilter } from '../types'; import { getFilters } from './get_filters'; diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.ts index a1b04cca0419d..8827a30d370d4 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_filters.ts @@ -6,7 +6,7 @@ import { badRequest } from 'boom'; import moment from 'moment-timezone'; -import { TimeRangeParams } from '../../../types'; +import { TimeRangeParams } from '../../common'; import { Filter, QueryFilter, SavedSearchObjectAttributes, SearchSourceFilter } from '../types'; export function getFilters( diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts index 9c45d23b13a37..cca79747110d5 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/types.d.ts @@ -4,20 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { JobParamPostPayload, TimeRangeParams } from '../../types'; +import { TimeRangeParams } from '../common'; export interface FakeRequest { - headers: Record; + headers: Record; } -export interface JobParamsPostPayloadPanelCsv extends JobParamPostPayload { +export interface JobParamsPanelCsvPost { + timerange?: TimeRangeParams; state?: any; } export interface SearchPanel { indexPatternSavedObjectId: string; attributes: SavedSearchObjectAttributes; - timerange: TimeRangeParams; + timerange?: TimeRangeParams; } export interface JobPayloadPanelCsv extends JobParamsPanelCsv { @@ -27,8 +28,7 @@ export interface JobPayloadPanelCsv extends JobParamsPanelCsv { export interface JobParamsPanelCsv { savedObjectType: string; savedObjectId: string; - isImmediate: boolean; - post?: JobParamsPostPayloadPanelCsv; + post?: JobParamsPanelCsvPost; visType?: string; } 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 3727b2ec7b432..eaaa11d461156 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,10 +7,11 @@ import { cryptoFactory } from '../../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; -import { JobParamsPNG } from '../types'; +import { JobParamsPNG, TaskPayloadPNG } from '../types'; export const createJobFnFactory: CreateJobFnFactory> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts index 67fc51bbfc352..e6b36643900dd 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts @@ -8,7 +8,8 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { PNG_JOB_TYPE } from '../../../../common/constants'; -import { RunTaskFn, RunTaskFnFactory, TaskRunResult } from '../../..//types'; +import { TaskRunResult } from '../../../lib/tasks'; +import { RunTaskFn, RunTaskFnFactory } from '../../../types'; import { decryptJobHeaders, getConditionalHeaders, @@ -18,12 +19,9 @@ import { import { generatePngObservableFactory } from '../lib/generate_png'; import { TaskPayloadPNG } from '../types'; -type QueuedPngExecutorFactory = RunTaskFnFactory>; - -export const runTaskFnFactory: QueuedPngExecutorFactory = function executeJobFactoryFn( - reporting, - parentLogger -) { +export const runTaskFnFactory: RunTaskFnFactory> = function executeJobFactoryFn(reporting, parentLogger) { const config = reporting.getConfig(); const encryptionKey = config.get('encryptionKey'); const logger = parentLogger.clone([PNG_JOB_TYPE, 'execute']); @@ -36,11 +34,11 @@ export const runTaskFnFactory: QueuedPngExecutorFactory = function executeJobFac const generatePngObservable = await generatePngObservableFactory(reporting); const jobLogger = logger.clone([jobId]); const process$: Rx.Observable = Rx.of(1).pipe( - mergeMap(() => decryptJobHeaders({ encryptionKey, job, logger })), - map((decryptedHeaders) => omitBlockedHeaders({ job, decryptedHeaders })), - map((filteredHeaders) => getConditionalHeaders({ config, job, filteredHeaders })), + mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, logger)), + map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)), + map((filteredHeaders) => getConditionalHeaders(config, filteredHeaders)), mergeMap((conditionalHeaders) => { - const urls = getFullUrls({ config, job }); + const urls = getFullUrls(config, job); const hashUrl = urls[0]; if (apmGetAssets) apmGetAssets.end(); @@ -60,7 +58,6 @@ export const runTaskFnFactory: QueuedPngExecutorFactory = function executeJobFac content_type: 'image/png', content: base64, size: (base64 && base64.length) || 0, - warnings, }; }), 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 1cc6836572b7b..50e09a9984b2c 100644 --- a/x-pack/plugins/reporting/server/export_types/png/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/index.ts @@ -19,9 +19,7 @@ import { metadata } from './metadata'; import { JobParamsPNG, TaskPayloadPNG } from './types'; export const getExportType = (): ExportTypeDefinition< - JobParamsPNG, CreateJobFn, - TaskPayloadPNG, RunTaskFn > => ({ ...metadata, diff --git a/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts b/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts index 096d0bd428214..786936d43424c 100644 --- a/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts +++ b/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts @@ -11,7 +11,7 @@ import { ReportingCore } from '../../../'; import { LevelLogger } from '../../../lib'; import { LayoutParams, PreserveLayout } from '../../../lib/layouts'; import { ScreenshotResults } from '../../../lib/screenshots'; -import { ConditionalHeaders } from '../../../types'; +import { ConditionalHeaders } from '../../common'; export async function generatePngObservableFactory(reporting: ReportingCore) { const getScreenshots = await reporting.getScreenshotsObservable(); @@ -19,7 +19,7 @@ export async function generatePngObservableFactory(reporting: ReportingCore) { return function generatePngObservable( logger: LevelLogger, url: string, - browserTimezone: string, + browserTimezone: string | undefined, conditionalHeaders: ConditionalHeaders, layoutParams: LayoutParams ): Rx.Observable<{ base64: string | null; warnings: string[] }> { diff --git a/x-pack/plugins/reporting/server/export_types/png/types.d.ts b/x-pack/plugins/reporting/server/export_types/png/types.d.ts index 373b709592ed2..1f99082c757c6 100644 --- a/x-pack/plugins/reporting/server/export_types/png/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/png/types.d.ts @@ -4,19 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BaseParams, BasePayload } from '../../../server/types'; import { LayoutParams } from '../../lib/layouts'; +import { BaseParams, BasePayload } from '../../types'; -// Job params: structure of incoming user request data -export interface JobParamsPNG extends BaseParams { - title: string; +interface BaseParamsPNG { + layout: LayoutParams; + forceNow?: string; relativeUrl: string; } +// Job params: structure of incoming user request data +export type JobParamsPNG = BaseParamsPNG & BaseParams; + // Job payload: structure of stored job data provided by create_job -export interface TaskPayloadPNG extends BasePayload { - browserTimezone: string; - forceNow?: string; - layout: LayoutParams; - relativeUrl: string; -} +export type TaskPayloadPNG = BaseParamsPNG & BasePayload; 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 cae706a479b7f..07eed00401bac 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,10 +7,11 @@ import { cryptoFactory } from '../../../lib'; import { CreateJobFn, CreateJobFnFactory } from '../../../types'; import { validateUrls } from '../../common'; -import { JobParamsPDF } from '../types'; +import { JobParamsPDF, TaskPayloadPDF } from '../types'; export const createJobFnFactory: CreateJobFnFactory> = function createJobFactoryFn(reporting) { const config = reporting.getConfig(); const crypto = cryptoFactory(config.get('encryptionKey')); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts index f3dc5bd656f73..ea0d60a9fad12 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts @@ -8,7 +8,8 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { catchError, map, mergeMap, takeUntil } from 'rxjs/operators'; import { PDF_JOB_TYPE } from '../../../../common/constants'; -import { RunTaskFn, RunTaskFnFactory, TaskRunResult } from '../../../types'; +import { TaskRunResult } from '../../../lib/tasks'; +import { RunTaskFn, RunTaskFnFactory } from '../../../types'; import { decryptJobHeaders, getConditionalHeaders, @@ -19,12 +20,9 @@ import { generatePdfObservableFactory } from '../lib/generate_pdf'; import { getCustomLogo } from '../lib/get_custom_logo'; import { TaskPayloadPDF } from '../types'; -type QueuedPdfExecutorFactory = RunTaskFnFactory>; - -export const runTaskFnFactory: QueuedPdfExecutorFactory = function executeJobFactoryFn( - reporting, - parentLogger -) { +export const runTaskFnFactory: RunTaskFnFactory> = function executeJobFactoryFn(reporting, parentLogger) { const config = reporting.getConfig(); const encryptionKey = config.get('encryptionKey'); @@ -39,12 +37,12 @@ export const runTaskFnFactory: QueuedPdfExecutorFactory = function executeJobFac const jobLogger = logger.clone([jobId]); const process$: Rx.Observable = Rx.of(1).pipe( - mergeMap(() => decryptJobHeaders({ encryptionKey, job, logger })), - map((decryptedHeaders) => omitBlockedHeaders({ job, decryptedHeaders })), - map((filteredHeaders) => getConditionalHeaders({ config, job, filteredHeaders })), + mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, logger)), + map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)), + map((filteredHeaders) => getConditionalHeaders(config, filteredHeaders)), mergeMap((conditionalHeaders) => getCustomLogo(reporting, conditionalHeaders, job.spaceId)), mergeMap(({ logo, conditionalHeaders }) => { - const urls = getFullUrls({ config, job }); + const urls = getFullUrls(config, job); const { browserTimezone, layout, title } = job; if (apmGetAssets) apmGetAssets.end(); 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 cf3ec9cdc8c2d..26704693ee489 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 @@ -19,9 +19,7 @@ import { metadata } from './metadata'; import { JobParamsPDF, TaskPayloadPDF } from './types'; export const getExportType = (): ExportTypeDefinition< - JobParamsPDF, CreateJobFn, - TaskPayloadPDF, RunTaskFn > => ({ ...metadata, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index 17624c1bedb57..2cf5b69835d1f 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -9,9 +9,9 @@ import * as Rx from 'rxjs'; import { mergeMap } from 'rxjs/operators'; import { ReportingCore } from '../../../'; import { LevelLogger } from '../../../lib'; -import { createLayout, LayoutInstance, LayoutParams } from '../../../lib/layouts'; +import { createLayout, LayoutParams } from '../../../lib/layouts'; import { ScreenshotResults } from '../../../lib/screenshots'; -import { ConditionalHeaders } from '../../../types'; +import { ConditionalHeaders } from '../../common'; // @ts-ignore untyped module import { pdf } from './pdf'; import { getTracker } from './tracker'; @@ -35,7 +35,7 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { logger: LevelLogger, title: string, urls: string[], - browserTimezone: string, + browserTimezone: string | undefined, conditionalHeaders: ConditionalHeaders, layoutParams: LayoutParams, logo?: string @@ -43,7 +43,7 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { const tracker = getTracker(); tracker.startLayout(); - const layout = createLayout(captureConfig, layoutParams) as LayoutInstance; + const layout = createLayout(captureConfig, layoutParams); tracker.endLayout(); tracker.startScreenshots(); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts index 8fa8fa5cbe3cb..426770d719069 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts @@ -11,7 +11,6 @@ import { createMockReportingCore, } from '../../../test_helpers'; import { getConditionalHeaders } from '../../common'; -import { TaskPayloadPDF } from '../types'; import { getCustomLogo } from './get_custom_logo'; let mockConfig: ReportingConfig; @@ -39,11 +38,7 @@ test(`gets logo from uiSettings`, async () => { get: mockGet, }); - const conditionalHeaders = getConditionalHeaders({ - job: {} as TaskPayloadPDF, - filteredHeaders: permittedHeaders, - config: mockConfig, - }); + const conditionalHeaders = getConditionalHeaders(mockConfig, permittedHeaders); const { logo } = await getCustomLogo(mockReportingPlugin, conditionalHeaders); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts index 35ab7001ecbe4..7bd1637db1379 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.ts @@ -6,7 +6,7 @@ import { ReportingCore } from '../../../'; import { UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../../../common/constants'; -import { ConditionalHeaders } from '../../../types'; +import { ConditionalHeaders } from '../../common'; export const getCustomLogo = async ( reporting: ReportingCore, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts index 7fd176e71f2d5..cef5c42856ff1 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/types.d.ts @@ -4,20 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BaseParams, BasePayload } from '../../../server/types'; -import { LayoutInstance, LayoutParams } from '../../lib/layouts'; +import { LayoutParams } from '../../lib/layouts'; +import { BaseParams, BasePayload } from '../../types'; -// Job params: structure of incoming user request data, after being parsed from RISON -export interface JobParamsPDF extends BaseParams { - title: string; +interface BaseParamsPDF { + layout: LayoutParams; + forceNow?: string; relativeUrls: string[]; - layout: LayoutInstance; } +// Job params: structure of incoming user request data, after being parsed from RISON +export type JobParamsPDF = BaseParamsPDF & BaseParams; + // Job payload: structure of stored job data provided by create_job -export interface TaskPayloadPDF extends BasePayload { - browserTimezone: string; - forceNow?: string; - layout: LayoutParams; - relativeUrls: string[]; -} +export type TaskPayloadPDF = BaseParamsPDF & BasePayload; diff --git a/x-pack/plugins/reporting/server/lib/check_license.ts b/x-pack/plugins/reporting/server/lib/check_license.ts index a764aa1f1eec6..1f8f66fe9b5ee 100644 --- a/x-pack/plugins/reporting/server/lib/check_license.ts +++ b/x-pack/plugins/reporting/server/lib/check_license.ts @@ -24,9 +24,7 @@ const messages = { }, }; -const makeManagementFeature = ( - exportTypes: Array> -) => { +const makeManagementFeature = (exportTypes: ExportTypeDefinition[]) => { return { id: 'management', checkLicense: (license?: ILicense) => { @@ -59,9 +57,7 @@ const makeManagementFeature = ( }; }; -const makeExportTypeFeature = ( - exportType: ExportTypeDefinition -) => { +const makeExportTypeFeature = (exportType: ExportTypeDefinition) => { return { id: exportType.id, checkLicense: (license?: ILicense) => { diff --git a/x-pack/plugins/reporting/server/lib/create_queue.ts b/x-pack/plugins/reporting/server/lib/create_queue.ts index 2da3d8bd47ccb..ded21d105f2f4 100644 --- a/x-pack/plugins/reporting/server/lib/create_queue.ts +++ b/x-pack/plugins/reporting/server/lib/create_queue.ts @@ -5,13 +5,13 @@ */ import { ReportingCore } from '../core'; -import { JobSource, TaskRunResult } from '../types'; import { createWorkerFactory } from './create_worker'; // @ts-ignore import { Esqueue } from './esqueue'; import { createTaggedLogger } from './esqueue/create_tagged_logger'; import { LevelLogger } from './level_logger'; -import { ReportingStore } from './store'; +import { ReportDocument, ReportingStore } from './store'; +import { TaskRunResult } from './tasks'; interface ESQueueWorker { on: (event: string, handler: any) => void; @@ -32,7 +32,7 @@ export interface ESQueueInstance { // GenericWorkerFn is a generic for ImmediateExecuteFn | ESQueueWorkerExecuteFn, type GenericWorkerFn = ( - jobSource: JobSource, + jobSource: ReportDocument, ...workerRestArgs: any[] ) => void | Promise; diff --git a/x-pack/plugins/reporting/server/lib/create_worker.ts b/x-pack/plugins/reporting/server/lib/create_worker.ts index c1c88dd8a54ba..7f03cefdb620e 100644 --- a/x-pack/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/plugins/reporting/server/lib/create_worker.ts @@ -9,10 +9,12 @@ import { PLUGIN_ID } from '../../common/constants'; import { durationToNumber } from '../../common/schema_utils'; import { ReportingCore } from '../../server'; import { LevelLogger } from '../../server/lib'; -import { ExportTypeDefinition, JobSource, RunTaskFn } from '../../server/types'; +import { RunTaskFn } from '../../server/types'; import { ESQueueInstance } from './create_queue'; // @ts-ignore untyped dependency import { events as esqueueEvents } from './esqueue'; +import { ReportDocument } from './store'; +import { ReportTaskParams } from './tasks'; export function createWorkerFactory(reporting: ReportingCore, logger: LevelLogger) { const config = reporting.getConfig(); @@ -23,18 +25,16 @@ export function createWorkerFactory(reporting: ReportingCore, log // Once more document types are added, this will need to be passed in return async function createWorker(queue: ESQueueInstance) { // export type / execute job map - const jobExecutors: Map> = new Map(); + const jobExecutors: Map = new Map(); - for (const exportType of reporting.getExportTypesRegistry().getAll() as Array< - ExportTypeDefinition> - >) { + for (const exportType of reporting.getExportTypesRegistry().getAll()) { const jobExecutor = exportType.runTaskFnFactory(reporting, logger); jobExecutors.set(exportType.jobType, jobExecutor); } - const workerFn = ( - jobSource: JobSource, - jobParams: TaskPayloadType, + const workerFn = ( + jobSource: ReportDocument, + payload: ReportTaskParams['payload'], cancellationToken: CancellationToken ) => { const { @@ -52,7 +52,7 @@ export function createWorkerFactory(reporting: ReportingCore, log } // pass the work to the jobExecutor - return jobTypeExecutor(jobId, jobParams, cancellationToken); + return jobTypeExecutor(jobId, payload, cancellationToken); }; const workerOptions = { diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.ts index 5acc6e38dddf9..305247e6f8637 100644 --- a/x-pack/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.ts @@ -6,7 +6,8 @@ import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { ReportingCore } from '../'; -import { BaseParams, CreateJobFn, ReportingUser } from '../types'; +import { durationToNumber } from '../../common/schema_utils'; +import { BaseParams, ReportingUser } from '../types'; import { LevelLogger } from './'; import { Report } from './store'; @@ -23,6 +24,13 @@ export function enqueueJobFactory( parentLogger: LevelLogger ): EnqueueJobFn { const logger = parentLogger.clone(['queue-job']); + const config = reporting.getConfig(); + const jobSettings = { + timeout: durationToNumber(config.get('queue', 'timeout')), + browser_type: config.get('capture', 'browser', 'type'), + max_attempts: config.get('capture', 'maxAttempts'), + priority: 10, // unused + }; return async function enqueueJob( exportTypeId: string, @@ -31,8 +39,6 @@ export function enqueueJobFactory( context: RequestHandlerContext, request: KibanaRequest ) { - type CreateJobFnType = CreateJobFn; - const exportType = reporting.getExportTypesRegistry().getById(exportTypeId); if (exportType == null) { @@ -40,15 +46,24 @@ export function enqueueJobFactory( } const [createJob, { store }] = await Promise.all([ - exportType.createJobFnFactory(reporting, logger) as CreateJobFnType, + exportType.createJobFnFactory(reporting, logger), reporting.getPluginStartDeps(), ]); - // add encrytped headers - const payload = await createJob(jobParams, context, request); + const job = await createJob(jobParams, context, request); + const pendingReport = new Report({ + jobtype: exportType.jobType, + created_by: user ? user.username : false, + payload: job, + meta: { + objectType: jobParams.objectType, + layout: jobParams.layout?.id, + }, + ...jobSettings, + }); // store the pending report, puts it in the Reporting Management UI table - const report = await store.addReport(exportType.jobType, user, payload); + const report = await store.addReport(pendingReport); logger.info(`Scheduled ${exportType.name} report: ${report._id}`); diff --git a/x-pack/plugins/reporting/server/lib/export_types_registry.ts b/x-pack/plugins/reporting/server/lib/export_types_registry.ts index 1159221a9224e..e93cdba48a26a 100644 --- a/x-pack/plugins/reporting/server/lib/export_types_registry.ts +++ b/x-pack/plugins/reporting/server/lib/export_types_registry.ts @@ -9,21 +9,16 @@ import { getExportType as getTypeCsv } from '../export_types/csv'; import { getExportType as getTypeCsvFromSavedObject } from '../export_types/csv_from_savedobject'; import { getExportType as getTypePng } from '../export_types/png'; import { getExportType as getTypePrintablePdf } from '../export_types/printable_pdf'; -import { ExportTypeDefinition } from '../types'; +import { CreateJobFn, ExportTypeDefinition } from '../types'; -type GetCallbackFn = ( - item: ExportTypeDefinition -) => boolean; -// => ExportTypeDefinition +type GetCallbackFn = (item: ExportTypeDefinition) => boolean; export class ExportTypesRegistry { - private _map: Map> = new Map(); + private _map: Map = new Map(); constructor() {} - register( - item: ExportTypeDefinition - ): void { + register(item: ExportTypeDefinition): void { if (!isString(item.id)) { throw new Error(`'item' must have a String 'id' property `); } @@ -43,35 +38,21 @@ export class ExportTypesRegistry { return this._map.size; } - getById( - id: string - ): ExportTypeDefinition { + getById(id: string): ExportTypeDefinition { if (!this._map.has(id)) { throw new Error(`Unknown id ${id}`); } - return this._map.get(id) as ExportTypeDefinition< - JobParamsType, - CreateJobFnType, - JobPayloadType, - RunTaskFnType - >; + return this._map.get(id) as ExportTypeDefinition; } - get( - findType: GetCallbackFn - ): ExportTypeDefinition { + get(findType: GetCallbackFn): ExportTypeDefinition { let result; for (const value of this._map.values()) { if (!findType(value)) { continue; // try next value } - const foundResult: ExportTypeDefinition< - JobParamsType, - CreateJobFnType, - JobPayloadType, - RunTaskFnType - > = value; + const foundResult: ExportTypeDefinition = value; if (result) { throw new Error('Found multiple items matching predicate.'); @@ -88,13 +69,19 @@ export class ExportTypesRegistry { } } +// TODO: Define a 2nd ExportTypeRegistry instance for "immediate execute" report job types only. +// It should not require a `CreateJobFn` for its ExportTypeDefinitions, which only makes sense for async. +// Once that is done, the `any` types below can be removed. + +/* + * @return ExportTypeRegistry: the ExportTypeRegistry instance that should be + * used to register async export type definitions + */ export function getExportTypesRegistry(): ExportTypesRegistry { const registry = new ExportTypesRegistry(); - - /* this replaces the previously async method of registering export types, - * where this would run a directory scan and types would be registered via - * discovery */ - const getTypeFns: Array<() => ExportTypeDefinition> = [ + type CreateFnType = CreateJobFn; // can not specify params types because different type of params are not assignable to each other + type RunFnType = any; // can not specify because ImmediateExecuteFn is not assignable to RunTaskFn + const getTypeFns: Array<() => ExportTypeDefinition> = [ getTypeCsv, getTypeCsvFromSavedObject, getTypePng, diff --git a/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts index 921d302387edf..585175aac82c5 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts @@ -6,12 +6,11 @@ import { CaptureConfig } from '../../types'; import { LayoutParams, LayoutTypes } from './'; -import { Layout } from './layout'; import { PreserveLayout } from './preserve_layout'; import { PrintLayout } from './print_layout'; -export function createLayout(captureConfig: CaptureConfig, layoutParams?: LayoutParams): Layout { - if (layoutParams && layoutParams.id === LayoutTypes.PRESERVE_LAYOUT) { +export function createLayout(captureConfig: CaptureConfig, layoutParams?: LayoutParams) { + if (layoutParams && layoutParams.dimensions && layoutParams.id === LayoutTypes.PRESERVE_LAYOUT) { return new PreserveLayout(layoutParams.dimensions); } diff --git a/x-pack/plugins/reporting/server/lib/layouts/index.ts b/x-pack/plugins/reporting/server/lib/layouts/index.ts index 507b7614072ea..c091339a60582 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/index.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/index.ts @@ -53,7 +53,7 @@ export interface Size { export interface LayoutParams { id: string; - dimensions: Size; + dimensions?: Size; selectors?: LayoutSelectorDictionary; } @@ -64,4 +64,4 @@ interface LayoutSelectors { positionElements?: (browser: HeadlessChromiumDriver, logger: LevelLogger) => Promise; } -export type LayoutInstance = Layout & LayoutSelectors & Size; +export type LayoutInstance = Layout & LayoutSelectors & Partial; diff --git a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts index e8d182dac0b1d..cecd761fbcf32 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts @@ -12,12 +12,13 @@ import { LayoutTypes, PageSizeParams, Size, + LayoutInstance, } from './'; // We use a zoom of two to bump up the resolution of the screenshot a bit. const ZOOM: number = 2; -export class PreserveLayout extends Layout { +export class PreserveLayout extends Layout implements LayoutInstance { public readonly selectors: LayoutSelectorDictionary = getDefaultLayoutSelectors(); public readonly groupCount = 1; public readonly height: number; diff --git a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts index b055fae8a780d..33f16bc7865d5 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts @@ -9,10 +9,16 @@ import { EvaluateFn, SerializableOrJSHandle } from 'puppeteer'; import { LevelLogger } from '../'; import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig } from '../../types'; -import { getDefaultLayoutSelectors, LayoutSelectorDictionary, LayoutTypes, Size } from './'; +import { + getDefaultLayoutSelectors, + LayoutInstance, + LayoutSelectorDictionary, + LayoutTypes, + Size, +} from './'; import { Layout } from './layout'; -export class PrintLayout extends Layout { +export class PrintLayout extends Layout implements LayoutInstance { public readonly selectors: LayoutSelectorDictionary = { ...getDefaultLayoutSelectors(), screenshot: '[data-shared-item]', diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_time_range.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_time_range.ts index afd6364454835..5f7919df4e9fd 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_time_range.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_time_range.ts @@ -5,7 +5,7 @@ */ import { LevelLogger, startTrace } from '../'; -import { LayoutInstance } from '../../../common/types'; +import { LayoutInstance } from '../layouts'; import { HeadlessChromiumDriver } from '../../browsers'; import { CONTEXT_GETTIMERANGE } from './constants'; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/index.ts b/x-pack/plugins/reporting/server/lib/screenshots/index.ts index c1d33cb519384..1b9722fb49458 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/index.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/index.ts @@ -6,7 +6,7 @@ import * as Rx from 'rxjs'; import { LevelLogger } from '../'; -import { ConditionalHeaders } from '../../types'; +import { ConditionalHeaders } from '../../export_types/common'; import { LayoutInstance } from '../layouts'; export { screenshotsObservableFactory } from './observable'; @@ -16,7 +16,7 @@ export interface ScreenshotObservableOpts { urls: string[]; conditionalHeaders: ConditionalHeaders; layout: LayoutInstance; - browserTimezone: string; + browserTimezone?: string; } export interface AttributesMap { diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts index 5b671e9f5b47e..798f926cd0a31 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts @@ -18,6 +18,7 @@ jest.mock('../../browsers/chromium/puppeteer', () => ({ import moment from 'moment'; import * as Rx from 'rxjs'; import { HeadlessChromiumDriver } from '../../browsers'; +import { ConditionalHeaders } from '../../export_types/common'; import { createMockBrowserDriverFactory, createMockConfig, @@ -25,7 +26,6 @@ import { createMockLayoutInstance, createMockLevelLogger, } from '../../test_helpers'; -import { ConditionalHeaders } from '../../types'; import { ElementsPositionAndAttribute } from './'; import * as contexts from './constants'; import { screenshotsObservableFactory } from './observable'; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts index e28f50851f4d9..e8b7f91764efd 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts @@ -5,10 +5,11 @@ */ import { i18n } from '@kbn/i18n'; -import { durationToNumber } from '../../../common/schema_utils'; import { LevelLogger, startTrace } from '../'; +import { durationToNumber } from '../../../common/schema_utils'; import { HeadlessChromiumDriver } from '../../browsers'; -import { CaptureConfig, ConditionalHeaders } from '../../types'; +import { ConditionalHeaders } from '../../export_types/common'; +import { CaptureConfig } from '../../types'; export const openUrl = async ( captureConfig: CaptureConfig, diff --git a/x-pack/plugins/reporting/server/lib/store/index.ts b/x-pack/plugins/reporting/server/lib/store/index.ts index a88d36d3fdf9a..a48f266120323 100644 --- a/x-pack/plugins/reporting/server/lib/store/index.ts +++ b/x-pack/plugins/reporting/server/lib/store/index.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { Report } from './report'; +export { Report, ReportDocument } from './report'; export { ReportingStore } from './store'; diff --git a/x-pack/plugins/reporting/server/lib/store/report.test.ts b/x-pack/plugins/reporting/server/lib/store/report.test.ts index 9ac5d1f87c387..1e4a833c7cabe 100644 --- a/x-pack/plugins/reporting/server/lib/store/report.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/report.test.ts @@ -14,7 +14,8 @@ describe('Class Report', () => { created_by: 'created_by_test_string', browser_type: 'browser_type_test_string', max_attempts: 50, - payload: { headers: 'payload_test_field', objectType: 'testOt' }, + payload: { headers: 'payload_test_field', objectType: 'testOt', title: 'cool report' }, + meta: { objectType: 'test' }, timeout: 30000, priority: 1, }); @@ -25,11 +26,10 @@ describe('Class Report', () => { attempts: 0, browser_type: 'browser_type_test_string', completed_at: undefined, - created_at: undefined, created_by: 'created_by_test_string', jobtype: 'test-report', max_attempts: 50, - meta: undefined, + meta: { objectType: 'test' }, payload: { headers: 'payload_test_field', objectType: 'testOt' }, priority: 1, started_at: undefined, @@ -38,12 +38,16 @@ describe('Class Report', () => { }, }); expect(report.toApiJSON()).toMatchObject({ + attempts: 0, browser_type: 'browser_type_test_string', created_by: 'created_by_test_string', + index: '.reporting-test-index-12345', jobtype: 'test-report', max_attempts: 50, payload: { headers: 'payload_test_field', objectType: 'testOt' }, + meta: { objectType: 'test' }, priority: 1, + status: 'pending', timeout: 30000, }); @@ -57,7 +61,8 @@ describe('Class Report', () => { created_by: 'created_by_test_string', browser_type: 'browser_type_test_string', max_attempts: 50, - payload: { headers: 'payload_test_field', objectType: 'testOt' }, + payload: { headers: 'payload_test_field', objectType: 'testOt', title: 'hot report' }, + meta: { objectType: 'stange' }, timeout: 30000, priority: 1, }); @@ -70,51 +75,46 @@ describe('Class Report', () => { }; report.updateWithEsDoc(metadata); - expect(report.toEsDocsJSON()).toMatchInlineSnapshot(` - Object { - "_id": "12342p9o387549o2345", - "_index": ".reporting-test-update", - "_source": Object { - "attempts": 0, - "browser_type": "browser_type_test_string", - "completed_at": undefined, - "created_at": undefined, - "created_by": "created_by_test_string", - "jobtype": "test-report", - "max_attempts": 50, - "meta": undefined, - "payload": Object { - "headers": "payload_test_field", - "objectType": "testOt", - }, - "priority": 1, - "started_at": undefined, - "status": "pending", - "timeout": 30000, - }, - } - `); - expect(report.toApiJSON()).toMatchInlineSnapshot(` - Object { - "attempts": 0, - "browser_type": "browser_type_test_string", - "completed_at": undefined, - "created_at": undefined, - "created_by": "created_by_test_string", - "id": "12342p9o387549o2345", - "index": ".reporting-test-update", - "jobtype": "test-report", - "max_attempts": 50, - "meta": undefined, - "payload": Object { - "headers": "payload_test_field", - "objectType": "testOt", - }, - "priority": 1, - "started_at": undefined, - "status": "pending", - "timeout": 30000, - } - `); + expect(report.toEsDocsJSON()).toMatchObject({ + _id: '12342p9o387549o2345', + _index: '.reporting-test-update', + _source: { + attempts: 0, + browser_type: 'browser_type_test_string', + completed_at: undefined, + created_by: 'created_by_test_string', + jobtype: 'test-report', + max_attempts: 50, + meta: { objectType: 'stange' }, + payload: { objectType: 'testOt' }, + priority: 1, + started_at: undefined, + status: 'pending', + timeout: 30000, + }, + }); + expect(report.toApiJSON()).toMatchObject({ + attempts: 0, + browser_type: 'browser_type_test_string', + completed_at: undefined, + created_by: 'created_by_test_string', + id: '12342p9o387549o2345', + index: '.reporting-test-update', + jobtype: 'test-report', + max_attempts: 50, + meta: { objectType: 'stange' }, + payload: { headers: 'payload_test_field', objectType: 'testOt' }, + priority: 1, + started_at: undefined, + status: 'pending', + timeout: 30000, + }); + }); + + it('throws error if converted to task JSON before being synced with ES storage', () => { + const report = new Report({} as any); + expect(() => report.updateWithEsDoc(report)).toThrowErrorMatchingInlineSnapshot( + `"Report object from ES has missing fields!"` + ); }); }); diff --git a/x-pack/plugins/reporting/server/lib/store/report.ts b/x-pack/plugins/reporting/server/lib/store/report.ts index 5c9b9ced7cce7..d82b90f4025ed 100644 --- a/x-pack/plugins/reporting/server/lib/store/report.ts +++ b/x-pack/plugins/reporting/server/lib/store/report.ts @@ -4,84 +4,96 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; // @ts-ignore no module definition import Puid from 'puid'; +import { JobStatus, ReportApiJSON } from '../../../common/types'; import { JobStatuses } from '../../../constants'; -import { LayoutInstance } from '../layouts'; +import { LayoutParams } from '../layouts'; +import { TaskRunResult } from '../tasks'; -/* - * The document created by Reporting to store in the .reporting index - */ -interface ReportingDocument { +interface ReportDocumentHead { _id: string; _index: string; _seq_no: unknown; _primary_term: unknown; +} + +/* + * The document created by Reporting to store in the .reporting index + */ +export interface ReportDocument extends ReportDocumentHead { + _source: ReportSource; +} + +export interface ReportSource { jobtype: string; + kibana_name: string; + kibana_id: string; created_by: string | false; payload: { headers: string; // encrypted headers + browserTimezone?: string; // may use timezone from advanced settings objectType: string; - layout?: LayoutInstance; + title: string; + layout?: LayoutParams; }; - meta: unknown; + meta: { objectType: string; layout?: string }; browser_type: string; max_attempts: number; timeout: number; - status: string; + status: JobStatus; attempts: number; - output?: unknown; + output: TaskRunResult | null; started_at?: string; completed_at?: string; - created_at?: string; + created_at: string; priority?: number; process_expiration?: string; } -/* - * The document created by Reporting to store as task parameters for Task - * Manager to reference the report in .reporting - */ const puid = new Puid(); -export class Report implements Partial { +export class Report implements Partial { public _index?: string; public _id: string; public _primary_term?: unknown; // set by ES public _seq_no: unknown; // set by ES - public readonly jobtype: string; - public readonly created_at?: string; - public readonly created_by?: string | false; - public readonly payload: { - headers: string; // encrypted headers - objectType: string; - layout?: LayoutInstance; - }; - public readonly meta: unknown; - public readonly max_attempts: number; - public readonly browser_type?: string; - - public readonly status: string; - public readonly attempts: number; - public readonly output?: unknown; - public readonly started_at?: string; - public readonly completed_at?: string; - public readonly process_expiration?: string; - public readonly priority?: number; - public readonly timeout?: number; + public readonly kibana_name: ReportSource['kibana_name']; + public readonly kibana_id: ReportSource['kibana_id']; + public readonly jobtype: ReportSource['jobtype']; + public readonly created_at: ReportSource['created_at']; + public readonly created_by: ReportSource['created_by']; + public readonly payload: ReportSource['payload']; + + public readonly meta: ReportSource['meta']; + public readonly max_attempts: ReportSource['max_attempts']; + public readonly browser_type?: ReportSource['browser_type']; + + public readonly status: ReportSource['status']; + public readonly attempts: ReportSource['attempts']; + public readonly output?: ReportSource['output']; + public readonly started_at?: ReportSource['started_at']; + public readonly completed_at?: ReportSource['completed_at']; + public readonly process_expiration?: ReportSource['process_expiration']; + public readonly priority?: ReportSource['priority']; + public readonly timeout?: ReportSource['timeout']; /* * Create an unsaved report + * Index string is required */ - constructor(opts: Partial) { + constructor(opts: Partial & Partial) { this._id = opts._id != null ? opts._id : puid.generate(); this._index = opts._index; this._primary_term = opts._primary_term; this._seq_no = opts._seq_no; this.payload = opts.payload!; + this.kibana_name = opts.kibana_name!; + this.kibana_id = opts.kibana_id!; this.jobtype = opts.jobtype!; this.max_attempts = opts.max_attempts!; this.attempts = opts.attempts || 0; @@ -89,9 +101,9 @@ export class Report implements Partial { this.process_expiration = opts.process_expiration; this.timeout = opts.timeout; - this.created_at = opts.created_at; - this.created_by = opts.created_by; - this.meta = opts.meta; + this.created_at = opts.created_at || moment.utc().toISOString(); + this.created_by = opts.created_by || false; + this.meta = opts.meta || { objectType: 'unknown' }; this.browser_type = opts.browser_type; this.priority = opts.priority; @@ -141,10 +153,12 @@ export class Report implements Partial { /* * Data structure for API responses */ - toApiJSON() { + toApiJSON(): ReportApiJSON { return { id: this._id, - index: this._index, + index: this._index!, + kibana_name: this.kibana_name, + kibana_id: this.kibana_id, jobtype: this.jobtype, created_at: this.created_at, created_by: this.created_by, diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts index 8dc4edd200052..931eae8b246c4 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts @@ -48,28 +48,25 @@ describe('ReportingStore', () => { describe('addReport', () => { it('returns Report object', async () => { const store = new ReportingStore(mockCore, mockLogger); - const reportType = 'unknowntype'; - const reportPayload = { - browserTimezone: 'UTC', - headers: 'rp_headers_1', - objectType: 'testOt', - }; - await expect( - store.addReport(reportType, { username: 'username1' }, reportPayload) - ).resolves.toMatchObject({ + const mockReport = new Report({ + _index: '.reporting-mock', + attempts: 0, + created_by: 'username1', + jobtype: 'unknowntype', + status: 'pending', + payload: {}, + meta: {}, + } as any); + await expect(store.addReport(mockReport)).resolves.toMatchObject({ _primary_term: undefined, _seq_no: undefined, attempts: 0, - browser_type: undefined, completed_at: undefined, created_by: 'username1', jobtype: 'unknowntype', - max_attempts: undefined, payload: {}, - priority: 10, - started_at: undefined, + meta: {}, status: 'pending', - timeout: 120000, }); }); @@ -83,15 +80,15 @@ describe('ReportingStore', () => { mockCore = await createMockReportingCore(mockConfig); const store = new ReportingStore(mockCore, mockLogger); - const reportType = 'unknowntype'; - const reportPayload = { - browserTimezone: 'UTC', - headers: 'rp_headers_2', - objectType: 'testOt', - }; - expect( - store.addReport(reportType, { username: 'user1' }, reportPayload) - ).rejects.toMatchInlineSnapshot(`[Error: Invalid index interval: centurially]`); + const mockReport = new Report({ + _index: '.reporting-errortest', + jobtype: 'unknowntype', + payload: {}, + meta: {}, + } as any); + expect(store.addReport(mockReport)).rejects.toMatchInlineSnapshot( + `[TypeError: this.client.callAsInternalUser is not a function]` + ); }); it('handles error creating the index', async () => { @@ -100,15 +97,15 @@ describe('ReportingStore', () => { callClusterStub.withArgs('indices.create').rejects(new Error('horrible error')); const store = new ReportingStore(mockCore, mockLogger); - const reportType = 'unknowntype'; - const reportPayload = { - browserTimezone: 'UTC', - headers: 'rp_headers_3', - objectType: 'testOt', - }; - await expect( - store.addReport(reportType, { username: 'user1' }, reportPayload) - ).rejects.toMatchInlineSnapshot(`[Error: horrible error]`); + const mockReport = new Report({ + _index: '.reporting-errortest', + jobtype: 'unknowntype', + payload: {}, + meta: {}, + } as any); + await expect(store.addReport(mockReport)).rejects.toMatchInlineSnapshot( + `[Error: horrible error]` + ); }); /* Creating the index will fail, if there were multiple jobs staged in @@ -123,15 +120,15 @@ describe('ReportingStore', () => { callClusterStub.withArgs('indices.create').rejects(new Error('devastating error')); const store = new ReportingStore(mockCore, mockLogger); - const reportType = 'unknowntype'; - const reportPayload = { - browserTimezone: 'UTC', - headers: 'rp_headers_4', - objectType: 'testOt', - }; - await expect( - store.addReport(reportType, { username: 'user1' }, reportPayload) - ).rejects.toMatchInlineSnapshot(`[Error: devastating error]`); + const mockReport = new Report({ + _index: '.reporting-mock', + jobtype: 'unknowntype', + payload: {}, + meta: {}, + } as any); + await expect(store.addReport(mockReport)).rejects.toMatchInlineSnapshot( + `[Error: devastating error]` + ); }); it('skips creating the index if already exists', async () => { @@ -142,28 +139,20 @@ describe('ReportingStore', () => { .rejects(new Error('resource_already_exists_exception')); // will be triggered but ignored const store = new ReportingStore(mockCore, mockLogger); - const reportType = 'unknowntype'; - const reportPayload = { - browserTimezone: 'UTC', - headers: 'rp_headers_5', - objectType: 'testOt', - }; - await expect( - store.addReport(reportType, { username: 'user1' }, reportPayload) - ).resolves.toMatchObject({ + const mockReport = new Report({ + created_by: 'user1', + jobtype: 'unknowntype', + payload: {}, + meta: {}, + } as any); + await expect(store.addReport(mockReport)).resolves.toMatchObject({ _primary_term: undefined, _seq_no: undefined, attempts: 0, - browser_type: undefined, - completed_at: undefined, created_by: 'user1', jobtype: 'unknowntype', - max_attempts: undefined, payload: {}, - priority: 10, - started_at: undefined, status: 'pending', - timeout: 120000, }); }); @@ -175,26 +164,24 @@ describe('ReportingStore', () => { .rejects(new Error('resource_already_exists_exception')); // will be triggered but ignored const store = new ReportingStore(mockCore, mockLogger); - const reportType = 'unknowntype'; - const reportPayload = { - browserTimezone: 'UTC', - headers: 'rp_test_headers', - objectType: 'testOt', - }; - await expect(store.addReport(reportType, false, reportPayload)).resolves.toMatchObject({ + const mockReport = new Report({ + _index: '.reporting-unsecured', + attempts: 0, + created_by: false, + jobtype: 'unknowntype', + payload: {}, + meta: {}, + status: 'pending', + } as any); + await expect(store.addReport(mockReport)).resolves.toMatchObject({ _primary_term: undefined, _seq_no: undefined, attempts: 0, - browser_type: undefined, - completed_at: undefined, created_by: false, jobtype: 'unknowntype', - max_attempts: undefined, + meta: {}, payload: {}, - priority: 10, - started_at: undefined, status: 'pending', - timeout: 120000, }); }); }); @@ -209,8 +196,10 @@ describe('ReportingStore', () => { browser_type: 'browser_type_test_string', max_attempts: 50, payload: { + title: 'test report', headers: 'rp_test_headers', objectType: 'testOt', + browserTimezone: 'ABC', }, timeout: 30000, priority: 1, @@ -248,8 +237,10 @@ describe('ReportingStore', () => { browser_type: 'browser_type_test_string', max_attempts: 50, payload: { + title: 'test report', headers: 'rp_test_headers', objectType: 'testOt', + browserTimezone: 'BCD', }, timeout: 30000, priority: 1, @@ -287,8 +278,10 @@ describe('ReportingStore', () => { browser_type: 'browser_type_test_string', max_attempts: 50, payload: { + title: 'test report', headers: 'rp_test_headers', objectType: 'testOt', + browserTimezone: 'CDE', }, timeout: 30000, priority: 1, @@ -326,8 +319,10 @@ describe('ReportingStore', () => { browser_type: 'browser_type_test_string', max_attempts: 50, payload: { + title: 'test report', headers: 'rp_test_headers', objectType: 'testOt', + browserTimezone: 'utc', }, timeout: 30000, priority: 1, diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index 03d88ca60e2c0..c20a9e991b4bc 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -5,21 +5,12 @@ */ import { ElasticsearchServiceSetup } from 'src/core/server'; -import { durationToNumber } from '../../../common/schema_utils'; import { LevelLogger, statuses } from '../'; import { ReportingCore } from '../../'; -import { BaseParams, BaseParamsEncryptedFields, ReportingUser } from '../../types'; import { indexTimestamp } from './index_timestamp'; import { mapping } from './mapping'; import { Report } from './report'; -interface JobSettings { - timeout: number; - browser_type: string; - max_attempts: number; - priority: number; -} - const checkReportIsEditable = (report: Report) => { if (!report._id || !report._index) { throw new Error(`Report object is not synced with ES!`); @@ -35,7 +26,6 @@ const checkReportIsEditable = (report: Report) => { export class ReportingStore { private readonly indexPrefix: string; private readonly indexInterval: string; - private readonly jobSettings: JobSettings; private client: ElasticsearchServiceSetup['legacy']['client']; private logger: LevelLogger; @@ -46,13 +36,6 @@ export class ReportingStore { this.client = elasticsearch.legacy.client; this.indexPrefix = config.get('index'); this.indexInterval = config.get('queue', 'indexInterval'); - this.jobSettings = { - timeout: durationToNumber(config.get('queue', 'timeout')), - browser_type: config.get('capture', 'browser', 'type'), - max_attempts: config.get('capture', 'maxAttempts'), - priority: 10, // unused - }; - this.logger = logger; } @@ -101,36 +84,17 @@ export class ReportingStore { * Called from addReport, which handles any errors */ private async indexReport(report: Report) { - const params = report.payload; - - // Queing is handled by TM. These queueing-based fields for reference in Report Info panel - const infoFields = { - timeout: report.timeout, - process_expiration: new Date(0), // use epoch so the job query works - created_at: new Date(), - attempts: 0, - max_attempts: report.max_attempts, - status: statuses.JOB_STATUS_PENDING, - browser_type: report.browser_type, - }; - - const indexParams = { + const doc = { index: report._index, id: report._id, body: { - ...infoFields, - jobtype: report.jobtype, - meta: { - // We are copying these values out of payload because these fields are indexed and can be aggregated on - // for tracking stats, while payload contents are not. - objectType: params.objectType, - layout: params.layout ? params.layout.id : 'none', - }, - payload: report.payload, - created_by: report.created_by, + ...report.toEsDocsJSON()._source, + process_expiration: new Date(0), // use epoch so the job query works + attempts: 0, + status: statuses.JOB_STATUS_PENDING, }, }; - return await this.client.callAsInternalUser('index', indexParams); + return await this.client.callAsInternalUser('index', doc); } /* @@ -140,23 +104,15 @@ export class ReportingStore { return await this.client.callAsInternalUser('indices.refresh', { index }); } - public async addReport( - type: string, - user: ReportingUser, - payload: BaseParams & BaseParamsEncryptedFields - ): Promise { - const timestamp = indexTimestamp(this.indexInterval); - const index = `${this.indexPrefix}-${timestamp}`; + public async addReport(report: Report): Promise { + let index = report._index; + if (!index) { + const timestamp = indexTimestamp(this.indexInterval); + index = `${this.indexPrefix}-${timestamp}`; + report._index = index; + } await this.createIndex(index); - const report = new Report({ - _index: index, - payload, - jobtype: type, - created_by: user ? user.username : false, - ...this.jobSettings, - }); - try { const doc = await this.indexReport(report); report.updateWithEsDoc(doc); @@ -166,7 +122,7 @@ export class ReportingStore { return report; } catch (err) { - this.logger.error(`Error in addReport!`); + this.logger.error(`Error in adding a report!`); this.logger.error(err); throw err; } @@ -220,7 +176,7 @@ export class ReportingStore { public async setReportCompleted(report: Report, stats: Partial): Promise { try { - const { output } = stats as { output: any }; + const { output } = stats; const status = output && output.warnings && output.warnings.length > 0 ? statuses.JOB_STATUS_WARNINGS diff --git a/x-pack/plugins/reporting/server/lib/tasks/index.ts b/x-pack/plugins/reporting/server/lib/tasks/index.ts new file mode 100644 index 0000000000000..0dd9945985bfb --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/tasks/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { BasePayload } from '../../types'; +import { ReportSource } from '../store/report'; + +/* + * The document created by Reporting to store as task parameters for Task + * Manager to reference the report in .reporting + */ +export interface ReportTaskParams { + id: string; + index?: string; // For ad-hoc, which as an existing "pending" record + payload: JobPayloadType; + created_at: ReportSource['created_at']; + created_by: ReportSource['created_by']; + jobtype: ReportSource['jobtype']; + attempts: ReportSource['attempts']; + meta: ReportSource['meta']; +} + +export interface TaskRunResult { + content_type: string | null; + content: string | null; + csv_contains_formulas?: boolean; + size: number; + max_size_reached?: boolean; + warnings?: string[]; +} diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts index 33620bc9a0038..dc4b30ffcfa7c 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts @@ -9,8 +9,8 @@ import { ReportingCore } from '../..'; import { API_DIAGNOSE_URL } from '../../../common/constants'; import { browserStartLogs } from '../../browsers/chromium/driver_factory/start_logs'; import { LevelLogger as Logger } from '../../lib'; -import { DiagnosticResponse } from '../../types'; import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing'; +import { DiagnosticResponse } from './'; const logsToHelpMap = { 'error while loading shared libraries': i18n.translate( diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/config.ts b/x-pack/plugins/reporting/server/routes/diagnostic/config.ts index 95c3a05bbf680..70428779366b3 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/config.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/config.ts @@ -10,8 +10,8 @@ import { defaults, get } from 'lodash'; import { ReportingCore } from '../..'; import { API_DIAGNOSE_URL } from '../../../common/constants'; import { LevelLogger as Logger } from '../../lib'; -import { DiagnosticResponse } from '../../types'; import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing'; +import { DiagnosticResponse } from './'; const KIBANA_MAX_SIZE_BYTES_PATH = 'csv.maxSizeBytes'; const ES_MAX_SIZE_BYTES_PATH = 'http.max_content_length'; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/index.ts b/x-pack/plugins/reporting/server/routes/diagnostic/index.ts index 895dee32614f1..84df91ea31b62 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/index.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/index.ts @@ -15,3 +15,9 @@ export const registerDiagnosticRoutes = (reporting: ReportingCore, logger: Logge registerDiagnoseConfig(reporting, logger); registerDiagnoseScreenshot(reporting, logger); }; + +export interface DiagnosticResponse { + help: string[]; + success: boolean; + logs: string; +} diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts index 0acf384869ded..6ea6e22c5d7f9 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts @@ -11,8 +11,8 @@ import { omitBlockedHeaders } from '../../export_types/common'; import { getAbsoluteUrlFactory } from '../../export_types/common/get_absolute_url'; import { generatePngObservableFactory } from '../../export_types/png/lib/generate_png'; import { LevelLogger as Logger } from '../../lib'; -import { DiagnosticResponse } from '../../types'; import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing'; +import { DiagnosticResponse } from './'; export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Logger) => { const setupDeps = reporting.getPluginSetupDeps(); @@ -54,10 +54,7 @@ export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Log }; const headers = { - headers: omitBlockedHeaders({ - job: null, - decryptedHeaders, - }), + headers: omitBlockedHeaders(decryptedHeaders), conditions: { hostname, port: +port, diff --git a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index 517f1dadc0ac1..400fbb16f54dc 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -10,17 +10,20 @@ import { ReportingCore } from '../'; import { API_BASE_GENERATE_V1 } from '../../common/constants'; import { createJobFnFactory } from '../export_types/csv_from_savedobject/create_job'; import { runTaskFnFactory } from '../export_types/csv_from_savedobject/execute_job'; -import { JobParamsPostPayloadPanelCsv } from '../export_types/csv_from_savedobject/types'; +import { + JobParamsPanelCsv, + JobParamsPanelCsvPost, +} from '../export_types/csv_from_savedobject/types'; import { LevelLogger as Logger } from '../lib'; -import { TaskRunResult } from '../types'; +import { TaskRunResult } from '../lib/tasks'; import { authorizedUserPreRoutingFactory } from './lib/authorized_user_pre_routing'; import { getJobParamsFromRequest } from './lib/get_job_params_from_request'; import { HandlerErrorFunction } from './types'; export type CsvFromSavedObjectRequest = KibanaRequest< - { savedObjectType: string; savedObjectId: string }, + JobParamsPanelCsv, unknown, - JobParamsPostPayloadPanelCsv + JobParamsPanelCsvPost >; /* @@ -66,27 +69,22 @@ export function registerGenerateCsvFromSavedObjectImmediate( }, userHandler(async (user, context, req: CsvFromSavedObjectRequest, res) => { const logger = parentLogger.clone(['savedobject-csv']); - const jobParams = getJobParamsFromRequest(req, { isImmediate: true }); + const jobParams = getJobParamsFromRequest(req); const createJob = createJobFnFactory(reporting, logger); const runTaskFn = runTaskFnFactory(reporting, logger); try { // FIXME: no create job for immediate download - const jobDocPayload = await createJob(jobParams, req.headers, context, req); + const payload = await createJob(jobParams, context, req); const { content_type: jobOutputContentType, content: jobOutputContent, size: jobOutputSize, - }: TaskRunResult = await runTaskFn(null, jobDocPayload, context, req); + }: TaskRunResult = await runTaskFn(null, payload, context, req); logger.info(`Job output size: ${jobOutputSize} bytes`); - /* - * ESQueue worker function defaults `content` to null, even if the - * runTask returned undefined. - * - * This converts null to undefined so the value can be sent to h.response() - */ + // convert null to undefined so the value can be sent to h.response() if (jobOutputContent === null) { logger.warn('CSV Job Execution created empty content result'); } diff --git a/x-pack/plugins/reporting/server/routes/generation.test.ts b/x-pack/plugins/reporting/server/routes/generation.test.ts index dd905223a81d5..867af75c8de27 100644 --- a/x-pack/plugins/reporting/server/routes/generation.test.ts +++ b/x-pack/plugins/reporting/server/routes/generation.test.ts @@ -74,8 +74,8 @@ describe('POST /api/reporting/generate', () => { jobContentEncoding: 'base64', jobContentExtension: 'pdf', validLicenses: ['basic', 'gold'], - createJobFnFactory: () => () => ({ jobParamsTest: { test1: 'yes' } }), - runTaskFnFactory: () => () => ({ runParamsTest: { test2: 'yes' } }), + createJobFnFactory: () => async () => ({ createJobTest: { test1: 'yes' } } as any), + runTaskFnFactory: () => async () => ({ runParamsTest: { test2: 'yes' } } as any), }); core.getExportTypesRegistry = () => mockExportTypesRegistry; }); @@ -163,9 +163,21 @@ describe('POST /api/reporting/generate', () => { .then(({ body }) => { expect(body).toMatchObject({ job: { - id: expect.any(String), + attempts: 0, + created_by: 'Tom Riddle', + id: 'foo', + index: 'foo-index', + jobtype: 'printable_pdf', + payload: { + createJobTest: { + test1: 'yes', + }, + }, + priority: 10, + status: 'pending', + timeout: 10000, }, - path: expect.any(String), + path: 'undefined/api/reporting/jobs/download/foo', }); }); }); diff --git a/x-pack/plugins/reporting/server/routes/index.ts b/x-pack/plugins/reporting/server/routes/index.ts index 11ad4cc9d4eb8..22edd4002dbcf 100644 --- a/x-pack/plugins/reporting/server/routes/index.ts +++ b/x-pack/plugins/reporting/server/routes/index.ts @@ -15,3 +15,10 @@ export function registerRoutes(reporting: ReportingCore, logger: Logger) { registerJobInfoRoutes(reporting); registerDiagnosticRoutes(reporting, logger); } + +export interface ReportingRequestPre { + management: { + jobTypes: string[]; + }; + user: string; +} diff --git a/x-pack/plugins/reporting/server/routes/jobs.test.ts b/x-pack/plugins/reporting/server/routes/jobs.test.ts index 187c69f4a72ef..fc1cfd00493c3 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.test.ts +++ b/x-pack/plugins/reporting/server/routes/jobs.test.ts @@ -25,7 +25,6 @@ describe('GET /api/reporting/jobs/download', () => { let core: ReportingCore; const config = createMockConfig(createMockConfigSchema()); - const getHits = (...sources: any) => { return { hits: { @@ -69,14 +68,14 @@ describe('GET /api/reporting/jobs/download', () => { jobType: 'unencodedJobType', jobContentExtension: 'csv', validLicenses: ['basic', 'gold'], - } as ExportTypeDefinition); + } as ExportTypeDefinition); exportTypesRegistry.register({ id: 'base64Encoded', jobType: 'base64EncodedJobType', jobContentEncoding: 'base64', jobContentExtension: 'pdf', validLicenses: ['basic', 'gold'], - } as ExportTypeDefinition); + } as ExportTypeDefinition); core.getExportTypesRegistry = () => exportTypesRegistry; }); diff --git a/x-pack/plugins/reporting/server/routes/jobs.ts b/x-pack/plugins/reporting/server/routes/jobs.ts index db62c0cc403fc..43e73c137fb13 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/plugins/reporting/server/routes/jobs.ts @@ -128,7 +128,7 @@ export function registerJobInfoRoutes(reporting: ReportingCore) { } return res.ok({ - body: jobOutput, + body: jobOutput || {}, headers: { 'content-type': 'application/json', }, diff --git a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts index 84a98d6d1f1d7..b154978d041f4 100644 --- a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -9,9 +9,9 @@ import contentDisposition from 'content-disposition'; import { get } from 'lodash'; import { CSV_JOB_TYPE } from '../../../common/constants'; import { ExportTypesRegistry, statuses } from '../../lib'; -import { ExportTypeDefinition, JobSource, TaskRunResult } from '../../types'; - -type ExportTypeType = ExportTypeDefinition; +import { ReportDocument } from '../../lib/store'; +import { TaskRunResult } from '../../lib/tasks'; +import { ExportTypeDefinition } from '../../types'; interface ErrorFromPayload { message: string; @@ -27,10 +27,10 @@ interface Payload { const DEFAULT_TITLE = 'report'; -const getTitle = (exportType: ExportTypeType, title?: string): string => +const getTitle = (exportType: ExportTypeDefinition, title?: string): string => `${title || DEFAULT_TITLE}.${exportType.jobContentExtension}`; -const getReportingHeaders = (output: TaskRunResult, exportType: ExportTypeType) => { +const getReportingHeaders = (output: TaskRunResult, exportType: ExportTypeDefinition) => { const metaDataHeaders: Record = {}; if (exportType.jobType === CSV_JOB_TYPE) { @@ -45,7 +45,10 @@ const getReportingHeaders = (output: TaskRunResult, exportType: ExportTypeType) }; export function getDocumentPayloadFactory(exportTypesRegistry: ExportTypesRegistry) { - function encodeContent(content: string | null, exportType: ExportTypeType): Buffer | string { + function encodeContent( + content: string | null, + exportType: ExportTypeDefinition + ): Buffer | string { switch (exportType.jobContentEncoding) { case 'base64': return content ? Buffer.from(content, 'base64') : ''; // convert null to empty string @@ -55,7 +58,9 @@ export function getDocumentPayloadFactory(exportTypesRegistry: ExportTypesRegist } function getCompleted(output: TaskRunResult, jobType: string, title: string): Payload { - const exportType = exportTypesRegistry.get((item: ExportTypeType) => item.jobType === jobType); + const exportType = exportTypesRegistry.get( + (item: ExportTypeDefinition) => item.jobType === jobType + ); const filename = getTitle(exportType, title); const headers = getReportingHeaders(output, exportType); @@ -92,16 +97,18 @@ export function getDocumentPayloadFactory(exportTypesRegistry: ExportTypesRegist }; } - return function getDocumentPayload(doc: JobSource): Payload { + return function getDocumentPayload(doc: ReportDocument): Payload { const { status, jobtype: jobType, payload: { title } = { title: '' } } = doc._source; const { output } = doc._source; - if (status === statuses.JOB_STATUS_COMPLETED || status === statuses.JOB_STATUS_WARNINGS) { - return getCompleted(output, jobType, title); - } + if (output) { + if (status === statuses.JOB_STATUS_COMPLETED || status === statuses.JOB_STATUS_WARNINGS) { + return getCompleted(output, jobType, title); + } - if (status === statuses.JOB_STATUS_FAILED) { - return getFailure(output); + if (status === statuses.JOB_STATUS_FAILED) { + return getFailure(output); + } } // send a 503 indicating that the report isn't completed yet diff --git a/x-pack/plugins/reporting/server/routes/lib/get_job_params_from_request.ts b/x-pack/plugins/reporting/server/routes/lib/get_job_params_from_request.ts index bfa15a4022a4d..e685339c966ed 100644 --- a/x-pack/plugins/reporting/server/routes/lib/get_job_params_from_request.ts +++ b/x-pack/plugins/reporting/server/routes/lib/get_job_params_from_request.ts @@ -7,17 +7,13 @@ import { JobParamsPanelCsv } from '../../export_types/csv_from_savedobject/types'; import { CsvFromSavedObjectRequest } from '../generate_from_savedobject_immediate'; -export function getJobParamsFromRequest( - request: CsvFromSavedObjectRequest, - opts: { isImmediate: boolean } -): JobParamsPanelCsv { +export function getJobParamsFromRequest(request: CsvFromSavedObjectRequest): JobParamsPanelCsv { const { savedObjectType, savedObjectId } = request.params; const { timerange, state } = request.body; const post = timerange || state ? { timerange, state } : undefined; return { - isImmediate: opts.isImmediate, savedObjectType, savedObjectId, post, diff --git a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts index b01c880abe820..d1270215b4821 100644 --- a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts +++ b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts @@ -8,7 +8,8 @@ import { i18n } from '@kbn/i18n'; import { errors as elasticsearchErrors } from 'elasticsearch'; import { get } from 'lodash'; import { ReportingCore } from '../../'; -import { JobSource, ReportingUser } from '../../types'; +import { ReportDocument } from '../../lib/store'; +import { ReportingUser } from '../../types'; const esErrors = elasticsearchErrors as Record; const defaultSize = 10; @@ -130,7 +131,7 @@ export function jobsQueryFactory(reportingCore: ReportingCore) { }); }, - get(user: ReportingUser, id: string, opts: GetOpts = {}): Promise | void> { + get(user: ReportingUser, id: string, opts: GetOpts = {}): Promise { if (!id) return Promise.resolve(); const username = getUsername(user); diff --git a/x-pack/plugins/reporting/server/routes/types.d.ts b/x-pack/plugins/reporting/server/routes/types.d.ts index 5c34d466197fe..b3f9225c3dce5 100644 --- a/x-pack/plugins/reporting/server/routes/types.d.ts +++ b/x-pack/plugins/reporting/server/routes/types.d.ts @@ -18,11 +18,11 @@ export type HandlerFunction = ( export type HandlerErrorFunction = (res: KibanaResponseFactory, err: Error) => any; -export interface QueuedJobPayload { +export interface QueuedJobPayload { error?: boolean; source: { job: { - payload: BasePayload; + payload: BasePayload; }; }; } diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index a3c63a0fb539d..eb046a3eab075 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -13,81 +13,11 @@ import { CancellationToken } from '../../../plugins/reporting/common'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { AuthenticatedUser, SecurityPluginSetup } from '../../security/server'; -import { JobStatus } from '../common/types'; import { ReportingConfigType } from './config'; import { ReportingCore } from './core'; import { LevelLogger } from './lib'; -import { LayoutInstance } from './lib/layouts'; - -/* - * Routing types - */ - -export interface ReportingRequestPre { - management: { - jobTypes: string[]; - }; - user: string; -} - -// generate a report with unparsed jobParams -export interface GenerateExportTypePayload { - jobParams: string; -} - -export type ReportingRequestPayload = GenerateExportTypePayload | JobParamPostPayload; - -export interface TimeRangeParams { - timezone: string; - min?: Date | string | number | null; - max?: Date | string | number | null; -} - -// the "raw" data coming from the client, unencrypted -export interface JobParamPostPayload { - timerange?: TimeRangeParams; -} - -// the pre-processed, encrypted data ready for storage -export interface BasePayload { - headers: string; // serialized encrypted headers - jobParams: JobParamsType; - title: string; - type: string; - spaceId?: string; -} - -export interface JobSource { - _id: string; - _index: string; - _source: { - jobtype: string; - output: TaskRunResult; - payload: BasePayload; - status: JobStatus; - }; -} - -export interface TaskRunResult { - content_type: string | null; - content: string | null; - csv_contains_formulas?: boolean; - size: number; - max_size_reached?: boolean; - warnings?: string[]; -} - -interface ConditionalHeadersConditions { - protocol: string; - hostname: string; - port: number; - basePath: string; -} - -export interface ConditionalHeaders { - headers: Record; - conditions: ConditionalHeadersConditions; -} +import { LayoutParams } from './lib/layouts'; +import { ReportTaskParams, TaskRunResult } from './lib/tasks'; /* * Plugin Contract @@ -118,24 +48,29 @@ export type CaptureConfig = ReportingConfigType['capture']; export type ScrollConfig = ReportingConfigType['csv']['scroll']; export interface BaseParams { - browserTimezone: string; - layout?: LayoutInstance; // for screenshot type reports + browserTimezone?: string; // browserTimezone is optional: it is not in old POST URLs that were generated prior to being added to this interface + layout?: LayoutParams; objectType: string; + title: string; } -export interface BaseParamsEncryptedFields extends BaseParams { - headers: string; // encrypted headers +// base params decorated with encrypted headers that come into runJob functions +export interface BasePayload extends BaseParams { + headers: string; + spaceId?: string; } -export type CreateJobFn = ( +// default fn type for CreateJobFnFactory +export type CreateJobFn = ( jobParams: JobParamsType, context: RequestHandlerContext, - request: KibanaRequest -) => Promise; + request: KibanaRequest +) => Promise; -export type RunTaskFn = ( +// default fn type for RunTaskFnFactory +export type RunTaskFn = ( jobId: string, - job: TaskPayloadType, + payload: ReportTaskParams['payload'], cancellationToken: CancellationToken ) => Promise; @@ -149,12 +84,7 @@ export type RunTaskFnFactory = ( logger: LevelLogger ) => RunTaskFnType; -export interface ExportTypeDefinition< - JobParamsType, - CreateJobFnType, - JobPayloadType, - RunTaskFnType -> { +export interface ExportTypeDefinition { id: string; name: string; jobType: string; @@ -164,9 +94,3 @@ export interface ExportTypeDefinition< runTaskFnFactory: RunTaskFnFactory; validLicenses: string[]; } - -export interface DiagnosticResponse { - help: string[]; - success: boolean; - logs: string; -} From e8f3529828b4c8a6c415f12f5f0b1b6dcb2da238 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Tue, 22 Sep 2020 16:17:35 -0400 Subject: [PATCH 18/92] [SECURITY_SOLUTION][ENDPOINT] Create Trusted Apps API changes to process user input (#78079) * Convert new trusted app data to expected format for artifact * Renamed condition field `process.path` to `process.path.text` * determine hash type based on length of hash value * Convert `process.hash.[sha1|md5|sha256]` to `process.hash.*` for return on list api * Add test for conversion of ExceptionItem to TrustedApp Item --- .../endpoint/schema/trusted_apps.test.ts | 4 +- .../common/endpoint/schema/trusted_apps.ts | 5 +- .../common/endpoint/types/trusted_apps.ts | 4 +- .../components/condition_entry.tsx | 2 +- .../routes/trusted_apps/trusted_apps.test.ts | 238 +++++++++++++++++- .../endpoint/routes/trusted_apps/utils.ts | 44 +++- 6 files changed, 279 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts index fc94e9a7c312a..c0fbebf73ed8a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.test.ts @@ -76,7 +76,7 @@ describe('When invoking Trusted Apps Schema', () => { os: 'windows', entries: [ { - field: 'process.path', + field: 'process.path.text', type: 'match', operator: 'included', value: 'c:/programs files/Anti-Virus', @@ -194,7 +194,7 @@ describe('When invoking Trusted Apps Schema', () => { }; expect(() => body.validate(bodyMsg2)).toThrow(); - ['process.hash.*', 'process.path'].forEach((field) => { + ['process.hash.*', 'process.path.text'].forEach((field) => { const bodyMsg3 = { ...getCreateTrustedAppItem(), entries: [ diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts index 72e24a7d694d4..3b3bec4a47804 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts @@ -26,7 +26,10 @@ export const PostTrustedAppCreateRequestSchema = { os: schema.oneOf([schema.literal('linux'), schema.literal('macos'), schema.literal('windows')]), entries: schema.arrayOf( schema.object({ - field: schema.oneOf([schema.literal('process.hash.*'), schema.literal('process.path')]), + field: schema.oneOf([ + schema.literal('process.hash.*'), + schema.literal('process.path.text'), + ]), type: schema.literal('match'), operator: schema.literal('included'), value: schema.string({ minLength: 1 }), diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index 3356fc67d2682..93e3305078f8d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -12,6 +12,7 @@ import { /** API request params for retrieving a list of Trusted Apps */ export type GetTrustedAppsListRequest = TypeOf; + export interface GetTrustedListAppsResponse { per_page: number; page: number; @@ -21,12 +22,13 @@ export interface GetTrustedListAppsResponse { /** API Request body for creating a new Trusted App entry */ export type PostTrustedAppCreateRequest = TypeOf; + export interface PostTrustedAppCreateResponse { data: TrustedApp; } export interface MacosLinuxConditionEntry { - field: 'process.hash.*' | 'process.path'; + field: 'process.hash.*' | 'process.path.text'; type: 'match'; operator: 'included'; value: string; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_entry.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_entry.tsx index 23bced0c048b1..7eeadeb02a385 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_entry.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_entry.tsx @@ -76,7 +76,7 @@ export const ConditionEntry = memo( 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.path', { defaultMessage: 'Path' } ), - value: 'process.path', + value: 'process.path.text', }, ]; }, []); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts index 35d0bf1116148..2368dcda09a38 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts @@ -26,7 +26,10 @@ import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/const import { EndpointAppContext } from '../../types'; import { ExceptionListClient, ListClient } from '../../../../../lists/server'; import { listMock } from '../../../../../lists/server/mocks'; -import { ExceptionListItemSchema } from '../../../../../lists/common/schemas/response'; +import { + ExceptionListItemSchema, + FoundExceptionListItemSchema, +} from '../../../../../lists/common/schemas/response'; import { DeleteTrustedAppsRequestParams } from './types'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; @@ -125,6 +128,97 @@ describe('when invoking endpoint trusted apps route handlers', () => { }); }); + it('should map Exception List Item to Trusted App item', async () => { + const request = createListRequest(10, 100); + const emptyResponse: FoundExceptionListItemSchema = { + data: [ + { + _tags: ['os:windows'], + _version: undefined, + comments: [], + created_at: '2020-09-21T19:43:48.240Z', + created_by: 'test', + description: '', + entries: [ + { + field: 'process.hash.sha256', + operator: 'included', + type: 'match', + value: 'a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476', + }, + { + field: 'process.hash.sha1', + operator: 'included', + type: 'match', + value: 'aedb279e378bed6c2db3c9dc9e12ba635e0b391c', + }, + { + field: 'process.hash.md5', + operator: 'included', + type: 'match', + value: '741462ab431a22233c787baab9b653c7', + }, + ], + id: '1', + item_id: '11', + list_id: 'trusted apps test', + meta: undefined, + name: 'test', + namespace_type: 'agnostic', + tags: [], + tie_breaker_id: '1', + type: 'simple', + updated_at: '2020-09-21T19:43:48.240Z', + updated_by: 'test', + }, + ], + page: 10, + per_page: 100, + total: 0, + }; + + exceptionsListClient.findExceptionListItem.mockResolvedValue(emptyResponse); + await routeHandler(context, request, response); + + expect(response.ok).toHaveBeenCalledWith({ + body: { + data: [ + { + created_at: '2020-09-21T19:43:48.240Z', + created_by: 'test', + description: '', + entries: [ + { + field: 'process.hash.*', + operator: 'included', + type: 'match', + value: 'a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476', + }, + { + field: 'process.hash.*', + operator: 'included', + type: 'match', + value: 'aedb279e378bed6c2db3c9dc9e12ba635e0b391c', + }, + { + field: 'process.hash.*', + operator: 'included', + type: 'match', + value: '741462ab431a22233c787baab9b653c7', + }, + ], + id: '1', + name: 'test', + os: 'windows', + }, + ], + page: 10, + per_page: 100, + total: 0, + }, + }); + }); + it('should log unexpected error if one occurs', async () => { exceptionsListClient.findExceptionListItem.mockImplementation(() => { throw new Error('expected error'); @@ -138,24 +232,26 @@ describe('when invoking endpoint trusted apps route handlers', () => { describe('when creating a trusted app', () => { let routeHandler: RequestHandler; - const createNewTrustedAppBody = (): PostTrustedAppCreateRequest => ({ + const createNewTrustedAppBody = (): { + -readonly [k in keyof PostTrustedAppCreateRequest]: PostTrustedAppCreateRequest[k]; + } => ({ name: 'Some Anti-Virus App', description: 'this one is ok', os: 'windows', entries: [ { - field: 'process.path', + field: 'process.path.text', type: 'match', operator: 'included', value: 'c:/programs files/Anti-Virus', }, ], }); - const createPostRequest = () => { + const createPostRequest = (body?: PostTrustedAppCreateRequest) => { return httpServerMock.createKibanaRequest({ path: TRUSTED_APPS_LIST_API, method: 'post', - body: createNewTrustedAppBody(), + body: body ?? createNewTrustedAppBody(), }); }; @@ -197,7 +293,7 @@ describe('when invoking endpoint trusted apps route handlers', () => { description: 'this one is ok', entries: [ { - field: 'process.path', + field: 'process.path.text', operator: 'included', type: 'match', value: 'c:/programs files/Anti-Virus', @@ -224,7 +320,7 @@ describe('when invoking endpoint trusted apps route handlers', () => { description: 'this one is ok', entries: [ { - field: 'process.path', + field: 'process.path.text', operator: 'included', type: 'match', value: 'c:/programs files/Anti-Virus', @@ -247,6 +343,134 @@ describe('when invoking endpoint trusted apps route handlers', () => { expect(response.internalError).toHaveBeenCalled(); expect(endpointAppContext.logFactory.get('trusted_apps').error).toHaveBeenCalled(); }); + + it('should trim trusted app entry name', async () => { + const newTrustedApp = createNewTrustedAppBody(); + newTrustedApp.name = `\n ${newTrustedApp.name} \r\n`; + const request = createPostRequest(newTrustedApp); + await routeHandler(context, request, response); + expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].name).toEqual( + 'Some Anti-Virus App' + ); + }); + + it('should trim condition entry values', async () => { + const newTrustedApp = createNewTrustedAppBody(); + newTrustedApp.entries.push({ + field: 'process.path.text', + value: '\n some value \r\n ', + operator: 'included', + type: 'match', + }); + const request = createPostRequest(newTrustedApp); + await routeHandler(context, request, response); + expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([ + { + field: 'process.path.text', + operator: 'included', + type: 'match', + value: 'c:/programs files/Anti-Virus', + }, + { + field: 'process.path.text', + value: 'some value', + operator: 'included', + type: 'match', + }, + ]); + }); + + it('should convert hash values to lowercase', async () => { + const newTrustedApp = createNewTrustedAppBody(); + newTrustedApp.entries.push({ + field: 'process.hash.*', + value: '741462AB431A22233C787BAAB9B653C7', + operator: 'included', + type: 'match', + }); + const request = createPostRequest(newTrustedApp); + await routeHandler(context, request, response); + expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([ + { + field: 'process.path.text', + operator: 'included', + type: 'match', + value: 'c:/programs files/Anti-Virus', + }, + { + field: 'process.hash.md5', + value: '741462ab431a22233c787baab9b653c7', + operator: 'included', + type: 'match', + }, + ]); + }); + + it('should detect md5 hash', async () => { + const newTrustedApp = createNewTrustedAppBody(); + newTrustedApp.entries = [ + { + field: 'process.hash.*', + value: '741462ab431a22233c787baab9b653c7', + operator: 'included', + type: 'match', + }, + ]; + const request = createPostRequest(newTrustedApp); + await routeHandler(context, request, response); + expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([ + { + field: 'process.hash.md5', + value: '741462ab431a22233c787baab9b653c7', + operator: 'included', + type: 'match', + }, + ]); + }); + + it('should detect sha1 hash', async () => { + const newTrustedApp = createNewTrustedAppBody(); + newTrustedApp.entries = [ + { + field: 'process.hash.*', + value: 'aedb279e378bed6c2db3c9dc9e12ba635e0b391c', + operator: 'included', + type: 'match', + }, + ]; + const request = createPostRequest(newTrustedApp); + await routeHandler(context, request, response); + expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([ + { + field: 'process.hash.sha1', + value: 'aedb279e378bed6c2db3c9dc9e12ba635e0b391c', + operator: 'included', + type: 'match', + }, + ]); + }); + + it('should detect sha256 hash', async () => { + const newTrustedApp = createNewTrustedAppBody(); + newTrustedApp.entries = [ + { + field: 'process.hash.*', + value: 'a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476', + operator: 'included', + type: 'match', + }, + ]; + const request = createPostRequest(newTrustedApp); + await routeHandler(context, request, response); + expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([ + { + field: 'process.hash.sha256', + value: 'a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476', + operator: 'included', + type: 'match', + }, + ]); + }); }); describe('when deleting a trusted app', () => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts index 794c1db4b49aa..2b8129ab950c6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts @@ -10,7 +10,7 @@ import { NewTrustedApp, TrustedApp } from '../../../../common/endpoint/types'; import { ExceptionListClient } from '../../../../../lists/server'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; -type NewExecptionItem = Parameters[0]; +type NewExceptionItem = Parameters[0]; /** * Map an ExcptionListItem to a TrustedApp item @@ -23,7 +23,15 @@ export const exceptionItemToTrustedAppItem = ( const { entries, description, created_by, created_at, name, _tags, id } = exceptionListItem; const os = osFromTagsList(_tags); return { - entries, + entries: entries.map((entry) => { + if (entry.field.startsWith('process.hash')) { + return { + ...entry, + field: 'process.hash.*', + }; + } + return entry; + }), description, created_at, created_by, @@ -51,22 +59,46 @@ export const newTrustedAppItemToExceptionItem = ({ entries, name, description = '', -}: NewTrustedApp): NewExecptionItem => { +}: NewTrustedApp): NewExceptionItem => { return { _tags: tagsListFromOs(os), comments: [], description, - entries, + // @ts-ignore + entries: entries.map(({ value, ...newEntry }) => { + let newValue = value.trim(); + + if (newEntry.field === 'process.hash.*') { + newValue = newValue.toLowerCase(); + newEntry.field = `process.hash.${hashType(newValue)}`; + } + + return { + ...newEntry, + value: newValue, + }; + }), itemId: uuid.v4(), listId: ENDPOINT_TRUSTED_APPS_LIST_ID, meta: undefined, - name, + name: name.trim(), namespaceType: 'agnostic', tags: [], type: 'simple', }; }; -const tagsListFromOs = (os: NewTrustedApp['os']): NewExecptionItem['_tags'] => { +const tagsListFromOs = (os: NewTrustedApp['os']): NewExceptionItem['_tags'] => { return [`os:${os}`]; }; + +const hashType = (hash: string): 'md5' | 'sha256' | 'sha1' | undefined => { + switch (hash.length) { + case 32: + return 'md5'; + case 40: + return 'sha1'; + case 64: + return 'sha256'; + } +}; From da1b3194195b4e53e64709e03ec619294e53dcf1 Mon Sep 17 00:00:00 2001 From: Rashmi Kulkarni Date: Tue, 22 Sep 2020 13:36:52 -0700 Subject: [PATCH 19/92] Accessibility - Dashboard Edit Panel tests (#78181) * accessibility tests for dashboard panel * added back the skipped test as it is still required to pass through th ea11ySnapshot * wip dashboard panel tests * wip- accessibility * wip -accessibility * wip accessibility * accessibility tests for dashboard edit panel * accessibility tests * removed the unused variables * dashboard_edit_panel tests * added a comment Co-authored-by: Elastic Machine --- test/accessibility/apps/dashboard_panel.ts | 2 +- .../apps/dashboard_edit_panel.ts | 131 ++++++++++++++++++ x-pack/test/accessibility/config.ts | 2 + 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 x-pack/test/accessibility/apps/dashboard_edit_panel.ts diff --git a/test/accessibility/apps/dashboard_panel.ts b/test/accessibility/apps/dashboard_panel.ts index 03fa76387da1f..1a817ce6b7a1c 100644 --- a/test/accessibility/apps/dashboard_panel.ts +++ b/test/accessibility/apps/dashboard_panel.ts @@ -18,7 +18,6 @@ */ import { FtrProviderContext } from '../ftr_provider_context'; - export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'home', 'settings']); const a11y = getService('a11y'); @@ -31,6 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, }); + await PageObjects.home.addSampleDataSet('flights'); await PageObjects.common.navigateToApp('dashboard'); await testSubjects.click('dashboardListingTitleLink-[Flights]-Global-Flight-Dashboard'); diff --git a/x-pack/test/accessibility/apps/dashboard_edit_panel.ts b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts new file mode 100644 index 0000000000000..1c3456ad8d593 --- /dev/null +++ b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const { dashboard } = getPageObjects(['dashboard']); + const a11y = getService('a11y'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + const drilldowns = getService('dashboardDrilldownsManage'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['security', 'common']); + const toasts = getService('toasts'); + + describe('Dashboard Edit Panel', () => { + before(async () => { + await esArchiver.load('dashboard/drilldowns'); + await esArchiver.loadIfNeeded('logstash_functional'); + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + await PageObjects.common.navigateToApp('dashboard'); + await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME); + await testSubjects.click('dashboardEditMode'); + }); + + after(async () => { + await esArchiver.unload('dashboard/drilldowns'); + }); + + // embeddable edit panel + it(' A11y test on dashboard edit panel menu options', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await a11y.testAppSnapshot(); + }); + + // https://github.com/elastic/kibana/issues/77931 + it.skip('A11y test for edit visualization and save', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelAction-editPanel'); + await testSubjects.click('visualizesaveAndReturnButton'); + await a11y.testAppSnapshot(); + }); + + // clone panel + it(' A11y test on dashboard embeddable clone panel', async () => { + await testSubjects.click('embeddablePanelAction-clonePanel'); + await a11y.testAppSnapshot(); + await toasts.dismissAllToasts(); + await dashboardPanelActions.removePanelByTitle('Visualization PieChart (copy)'); + }); + + // edit panel title + it(' A11y test on dashboard embeddable edit dashboard title', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelAction-ACTION_CUSTOMIZE_PANEL'); + await a11y.testAppSnapshot(); + await testSubjects.click('customizePanelHideTitle'); + await a11y.testAppSnapshot(); + await testSubjects.click('saveNewTitleButton'); + }); + + // create drilldown + it('A11y test on dashboard embeddable open flyout and drilldown', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelAction-OPEN_FLYOUT_ADD_DRILLDOWN'); + await a11y.testAppSnapshot(); + await testSubjects.click('flyoutCloseButton'); + }); + + // clicking on more button + it('A11y test on dashboard embeddable more button', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelMore-mainMenu'); + await a11y.testAppSnapshot(); + }); + + // https://github.com/elastic/kibana/issues/77422 + it.skip('A11y test on dashboard embeddable custom time range', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelAction-CUSTOM_TIME_RANGE'); + await a11y.testAppSnapshot(); + }); + + // flow will change whenever the custom time range a11y issue gets fixed. + // Will need to click on gear icon and then click on more. + + // inspector panel + it('A11y test on dashboard embeddable open inspector', async () => { + await testSubjects.click('embeddablePanelAction-openInspector'); + await a11y.testAppSnapshot(); + await testSubjects.click('euiFlyoutCloseButton'); + }); + + // fullscreen + it('A11y test on dashboard embeddable fullscreen', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelMore-mainMenu'); + await testSubjects.click('embeddablePanelAction-togglePanel'); + await a11y.testAppSnapshot(); + }); + + // minimize fullscreen panel + it('A11y test on dashboard embeddable fullscreen minimize ', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelMore-mainMenu'); + await testSubjects.click('embeddablePanelAction-togglePanel'); + await a11y.testAppSnapshot(); + }); + + // replace panel + it('A11y test on dashboard embeddable replace panel', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelMore-mainMenu'); + await testSubjects.click('embeddablePanelAction-replacePanel'); + await a11y.testAppSnapshot(); + await testSubjects.click('euiFlyoutCloseButton'); + }); + + // delete from dashboard + it('A11y test on dashboard embeddable delete panel', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelMore-mainMenu'); + await testSubjects.click('embeddablePanelAction-deletePanel'); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index bae7b688fd28c..1163b74b24628 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -21,7 +21,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/search_profiler'), require.resolve('./apps/uptime'), require.resolve('./apps/spaces'), + require.resolve('./apps/dashboard_edit_panel'), ], + pageObjects, services, From ac00887acd6b97e1b7a44e1fd1fdf32031f8c911 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Tue, 22 Sep 2020 15:41:28 -0500 Subject: [PATCH 20/92] Update to latest rum-react (#78193) * Update to latest rum-react The latest version fixes a problem where you would get a bunch of warnings that you couldn't turn off if you used `render` instead of component with a route. This was causing us to use `component` in some places where `render` should be used. The latest version fixes this problem so we change back to `render` where appropriate. Also make our `ApmRoute` a `Route` instead of `any`. --- x-pack/package.json | 2 +- .../app/Main/route_config/index.tsx | 15 +++-------- .../Main/route_config/route_config.test.tsx | 6 ++--- x-pack/plugins/apm/typings/apm_rum_react.d.ts | 5 +++- yarn.lock | 26 +++++++++++++++---- 5 files changed, 31 insertions(+), 23 deletions(-) diff --git a/x-pack/package.json b/x-pack/package.json index 0560b1bebe42b..6593f04bade27 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "@cypress/webpack-preprocessor": "^4.1.0", - "@elastic/apm-rum-react": "^1.2.3", + "@elastic/apm-rum-react": "^1.2.4", "@elastic/maki": "6.3.0", "@kbn/dev-utils": "1.0.0", "@kbn/es": "1.0.0", diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx index 4aa2d841e8deb..0d61ca8e39845 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -62,15 +62,6 @@ export function renderAsRedirectTo(to: string) { // If you provide an inline function to the component prop, you would create a // new component every render. This results in the existing component unmounting // and the new component mounting instead of just updating the existing component. -// -// This means you should use `render` if you're providing an inline function. -// However, the `ApmRoute` component from @elastic/apm-rum-react, only supports -// `component`, and will give you a large console warning if you use `render`. -// -// This warning cannot be turned off -// (see https://github.com/elastic/apm-agent-rum-js/issues/881) so while this is -// slightly more code, it provides better performance without causing console -// warnings to appear. function HomeServices() { return ; } @@ -153,7 +144,7 @@ export const routes: APMRouteDefinition[] = [ { exact: true, path: '/', - component: renderAsRedirectTo('/services'), + render: renderAsRedirectTo('/services'), breadcrumb: 'APM', }, { @@ -175,7 +166,7 @@ export const routes: APMRouteDefinition[] = [ { exact: true, path: '/settings', - component: renderAsRedirectTo('/settings/agent-configuration'), + render: renderAsRedirectTo('/settings/agent-configuration'), breadcrumb: i18n.translate('xpack.apm.breadcrumb.listSettingsTitle', { defaultMessage: 'Settings', }), @@ -219,7 +210,7 @@ export const routes: APMRouteDefinition[] = [ exact: true, path: '/services/:serviceName', breadcrumb: ({ match }) => match.params.serviceName, - component: (props: RouteComponentProps<{ serviceName: string }>) => + render: (props: RouteComponentProps<{ serviceName: string }>) => renderAsRedirectTo( `/services/${props.match.params.serviceName}/transactions` )(props), diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx index 21a162111bc79..ba3641cc4dadd 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx @@ -14,7 +14,7 @@ describe('routes', () => { it('redirects to /services', () => { const location = { hash: '', pathname: '/', search: '' }; expect( - (route as any).component({ location } as any).props.to.pathname + (route!.render!({ location } as any) as any).props.to.pathname ).toEqual('/services'); }); }); @@ -28,9 +28,7 @@ describe('routes', () => { search: '', }; - expect( - ((route as any).component({ location }) as any).props.to - ).toEqual({ + expect((route!.render!({ location } as any) as any).props.to).toEqual({ hash: '', pathname: '/services/opbeans-python/transactions/view', search: diff --git a/x-pack/plugins/apm/typings/apm_rum_react.d.ts b/x-pack/plugins/apm/typings/apm_rum_react.d.ts index 1c3e41ec12780..f9eafef59f55d 100644 --- a/x-pack/plugins/apm/typings/apm_rum_react.d.ts +++ b/x-pack/plugins/apm/typings/apm_rum_react.d.ts @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + declare module '@elastic/apm-rum-react' { - export const ApmRoute: any; + import { RouteProps } from 'react-router-dom'; + + export const ApmRoute: React.ComponentClass; } diff --git a/yarn.lock b/yarn.lock index 9e96158771cde..48d675eb5dadd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1129,12 +1129,21 @@ opentracing "^0.14.3" promise-polyfill "^8.1.3" -"@elastic/apm-rum-react@^1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-1.2.3.tgz#fdf28492daca0ee6aa67c53a457eea1f16739e1e" - integrity sha512-oCjF/L46OYDRLHKt60l7aU+DFE484dwb/kKN12VZCOgueDZm4BCJd7yaosBtWDhnw0tl0Iqc0X3r4U7pQ+g9aA== +"@elastic/apm-rum-core@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.6.1.tgz#0870e654e84e1f2ffea7c8a247a2da1b72918bcd" + integrity sha512-UtWj8UNN1sfSjav1kQK2NFhHtrH++4FzhtY0g80aSfHrDdBKVXaecWswoGmK3aiGJ9LAVlAXNfF3tPMT6JN23g== + dependencies: + error-stack-parser "^1.3.5" + opentracing "^0.14.3" + promise-polyfill "^8.1.3" + +"@elastic/apm-rum-react@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-1.2.4.tgz#f5b908f69f2696af10d19250226559ceb33dc1e9" + integrity sha512-zjig55n4/maU+kAEePS+DxgD12t4J0X9t3tB9YuO0gUIJhgT7KTL1Nv93ZmJ3u2tCJSpdYVfKQ0GBgSfjt1vVQ== dependencies: - "@elastic/apm-rum" "^5.5.0" + "@elastic/apm-rum" "^5.6.0" hoist-non-react-statics "^3.3.0" "@elastic/apm-rum@^5.5.0": @@ -1144,6 +1153,13 @@ dependencies: "@elastic/apm-rum-core" "^5.6.0" +"@elastic/apm-rum@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-5.6.0.tgz#0af2acb55091b9eb315cf38c6422a83cddfecb6f" + integrity sha512-6CuODbt7dBXoqsKoqhshQQC4GyqsGMPOR1FXZCWbnq55UZq1TWqra6zNCtEEFinz8rPaww7bzmNciXKRvGjIzQ== + dependencies: + "@elastic/apm-rum-core" "^5.6.1" + "@elastic/charts@21.1.2": version "21.1.2" resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-21.1.2.tgz#da7e9c1025bf730a738b6ac6d7024d97dd2b5aa2" From 0f8bbf11f4eed415904f3281ff6fa667786bb62f Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 22 Sep 2020 14:12:52 -0700 Subject: [PATCH 21/92] Reporting/Docs: Updates for setting to enable CSV Download (#78101) * Reporting/Docs - update documentation about setting to enable CSV Download * break up long setting name to fix table scrolling --- docs/settings/reporting-settings.asciidoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 3489dcd018293..27ef089f5847d 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -229,10 +229,10 @@ a| `xpack.reporting.capture.browser` See OWASP: https://www.owasp.org/index.php/CSV_Injection Defaults to `true`. -| `xpack.reporting.csv.enablePanelActionDownload` - | Enables CSV export from a saved search on a dashboard. This action is available in the dashboard - panel menu for the saved search. - Defaults to `true`. +| `xpack.reporting.csv` `.enablePanelActionDownload` + | Enables CSV export from a saved search on a dashboard. This action is available in the dashboard panel menu for the saved search. + *Note:* This setting exists for backwards compatibility, but is unused and hardcoded to `true`. CSV export from a saved search on a dashboard + is enabled when Reporting is enabled. |=== From a0f03ddbd176b1afc72ca5581d9024564a0fe6ca Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Tue, 22 Sep 2020 16:21:09 -0500 Subject: [PATCH 22/92] [Metrics UI] Add inventory view timeline (#77804) Co-authored-by: Elastic Machine --- .../infra/common/http_api/snapshot_api.ts | 2 +- .../common/inventory_models/intl_strings.ts | 115 +-------- .../infra/common/inventory_models/types.ts | 5 +- .../infra/common/snapshot_metric_i18n.ts | 79 ++++-- .../saved_views/toolbar_control.tsx | 28 +-- .../components/bottom_drawer.tsx | 87 +++++++ .../inventory_view/components/layout.tsx | 52 ++-- .../components/timeline/timeline.tsx | 228 ++++++++++++++++++ .../components/waffle/interval_label.tsx | 2 +- .../inventory_view/hooks/use_snaphot.ts | 2 +- .../inventory_view/hooks/use_timeline.ts | 121 ++++++++++ .../components/helpers/get_chart_theme.ts | 27 ++- ...ransform_request_to_metrics_api_request.ts | 6 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 15 files changed, 568 insertions(+), 188 deletions(-) create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_timeline.ts diff --git a/x-pack/plugins/infra/common/http_api/snapshot_api.ts b/x-pack/plugins/infra/common/http_api/snapshot_api.ts index e1b8dfa4770ba..a6273fa967baf 100644 --- a/x-pack/plugins/infra/common/http_api/snapshot_api.ts +++ b/x-pack/plugins/infra/common/http_api/snapshot_api.ts @@ -99,7 +99,7 @@ export const SnapshotRequestRT = rt.intersection([ rt.type({ timerange: InfraTimerangeInputRT, metrics: rt.array(SnapshotMetricInputRT), - groupBy: SnapshotGroupByRT, + groupBy: rt.union([SnapshotGroupByRT, rt.null]), nodeType: ItemTypeRT, sourceId: rt.string, }), diff --git a/x-pack/plugins/infra/common/inventory_models/intl_strings.ts b/x-pack/plugins/infra/common/inventory_models/intl_strings.ts index 2a885136f4ee7..cd7409100160d 100644 --- a/x-pack/plugins/infra/common/inventory_models/intl_strings.ts +++ b/x-pack/plugins/infra/common/inventory_models/intl_strings.ts @@ -5,36 +5,8 @@ */ import { i18n } from '@kbn/i18n'; -import { SnapshotMetricType } from './types'; -export const CPUUsage = i18n.translate('xpack.infra.waffle.metricOptions.cpuUsageText', { - defaultMessage: 'CPU usage', -}); - -export const MemoryUsage = i18n.translate('xpack.infra.waffle.metricOptions.memoryUsageText', { - defaultMessage: 'Memory usage', -}); - -export const InboundTraffic = i18n.translate( - 'xpack.infra.waffle.metricOptions.inboundTrafficText', - { - defaultMessage: 'Inbound traffic', - } -); - -export const OutboundTraffic = i18n.translate( - 'xpack.infra.waffle.metricOptions.outboundTrafficText', - { - defaultMessage: 'Outbound traffic', - } -); - -export const LogRate = i18n.translate('xpack.infra.waffle.metricOptions.hostLogRateText', { - defaultMessage: 'Log rate', -}); - -export const Load = i18n.translate('xpack.infra.waffle.metricOptions.loadText', { - defaultMessage: 'Load', -}); +import { toMetricOpt } from '../snapshot_metric_i18n'; +import { SnapshotMetricType, SnapshotMetricTypeKeys } from './types'; interface Lookup { [id: string]: string; @@ -70,80 +42,9 @@ export const fieldToName = (field: string) => { return LOOKUP[field] || field; }; -export const SNAPSHOT_METRIC_TRANSLATIONS = { - cpu: i18n.translate('xpack.infra.waffle.metricOptions.cpuUsageText', { - defaultMessage: 'CPU usage', - }), - - memory: i18n.translate('xpack.infra.waffle.metricOptions.memoryUsageText', { - defaultMessage: 'Memory usage', - }), - - rx: i18n.translate('xpack.infra.waffle.metricOptions.inboundTrafficText', { - defaultMessage: 'Inbound traffic', - }), - - tx: i18n.translate('xpack.infra.waffle.metricOptions.outboundTrafficText', { - defaultMessage: 'Outbound traffic', - }), - - logRate: i18n.translate('xpack.infra.waffle.metricOptions.hostLogRateText', { - defaultMessage: 'Log rate', - }), - - load: i18n.translate('xpack.infra.waffle.metricOptions.loadText', { - defaultMessage: 'Load', - }), - - count: i18n.translate('xpack.infra.waffle.metricOptions.countText', { - defaultMessage: 'Count', - }), - diskIOReadBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOReadBytes', { - defaultMessage: 'Disk Reads', - }), - diskIOWriteBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOWriteBytes', { - defaultMessage: 'Disk Writes', - }), - s3BucketSize: i18n.translate('xpack.infra.waffle.metricOptions.s3BucketSize', { - defaultMessage: 'Bucket Size', - }), - s3TotalRequests: i18n.translate('xpack.infra.waffle.metricOptions.s3TotalRequests', { - defaultMessage: 'Total Requests', - }), - s3NumberOfObjects: i18n.translate('xpack.infra.waffle.metricOptions.s3NumberOfObjects', { - defaultMessage: 'Number of Objects', - }), - s3DownloadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3DownloadBytes', { - defaultMessage: 'Downloads (Bytes)', - }), - s3UploadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3UploadBytes', { - defaultMessage: 'Uploads (Bytes)', - }), - rdsConnections: i18n.translate('xpack.infra.waffle.metricOptions.rdsConnections', { - defaultMessage: 'Connections', - }), - rdsQueriesExecuted: i18n.translate('xpack.infra.waffle.metricOptions.rdsQueriesExecuted', { - defaultMessage: 'Queries Executed', - }), - rdsActiveTransactions: i18n.translate('xpack.infra.waffle.metricOptions.rdsActiveTransactions', { - defaultMessage: 'Active Transactions', - }), - rdsLatency: i18n.translate('xpack.infra.waffle.metricOptions.rdsLatency', { - defaultMessage: 'Latency', - }), - sqsMessagesVisible: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesVisible', { - defaultMessage: 'Messages Available', - }), - sqsMessagesDelayed: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesDelayed', { - defaultMessage: 'Messages Delayed', - }), - sqsMessagesSent: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesSent', { - defaultMessage: 'Messages Added', - }), - sqsMessagesEmpty: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesEmpty', { - defaultMessage: 'Messages Returned Empty', - }), - sqsOldestMessage: i18n.translate('xpack.infra.waffle.metricOptions.sqsOldestMessage', { - defaultMessage: 'Oldest Message', - }), -} as Record; +const snapshotTypeKeys = Object.keys(SnapshotMetricTypeKeys) as SnapshotMetricType[]; +export const SNAPSHOT_METRIC_TRANSLATIONS = snapshotTypeKeys.reduce((result, metric) => { + const text = toMetricOpt(metric)?.text; + if (text) return { ...result, [metric]: text }; + return result; +}, {}) as Record; diff --git a/x-pack/plugins/infra/common/inventory_models/types.ts b/x-pack/plugins/infra/common/inventory_models/types.ts index 851646ef1fa12..7eb74056dcf28 100644 --- a/x-pack/plugins/infra/common/inventory_models/types.ts +++ b/x-pack/plugins/infra/common/inventory_models/types.ts @@ -314,7 +314,7 @@ export const ESAggregationRT = rt.union([ export const MetricsUIAggregationRT = rt.record(rt.string, ESAggregationRT); export type MetricsUIAggregation = rt.TypeOf; -export const SnapshotMetricTypeRT = rt.keyof({ +export const SnapshotMetricTypeKeys = { count: null, cpu: null, load: null, @@ -339,7 +339,8 @@ export const SnapshotMetricTypeRT = rt.keyof({ sqsMessagesEmpty: null, sqsOldestMessage: null, custom: null, -}); +}; +export const SnapshotMetricTypeRT = rt.keyof(SnapshotMetricTypeKeys); export type SnapshotMetricType = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/snapshot_metric_i18n.ts b/x-pack/plugins/infra/common/snapshot_metric_i18n.ts index 412c60fd9a1a7..60454e770584e 100644 --- a/x-pack/plugins/infra/common/snapshot_metric_i18n.ts +++ b/x-pack/plugins/infra/common/snapshot_metric_i18n.ts @@ -4,204 +4,235 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; +import { mapValues } from 'lodash'; import { SnapshotMetricType } from './inventory_models/types'; -const Translations = { +// Lowercase versions of all metrics, for when they need to be used in the middle of a sentence; +// these may need to be translated differently depending on language, e.g. still capitalizing "CPU" +const TranslationsLowercase = { CPUUsage: i18n.translate('xpack.infra.waffle.metricOptions.cpuUsageText', { defaultMessage: 'CPU usage', }), MemoryUsage: i18n.translate('xpack.infra.waffle.metricOptions.memoryUsageText', { - defaultMessage: 'Memory usage', + defaultMessage: 'memory usage', }), InboundTraffic: i18n.translate('xpack.infra.waffle.metricOptions.inboundTrafficText', { - defaultMessage: 'Inbound traffic', + defaultMessage: 'inbound traffic', }), OutboundTraffic: i18n.translate('xpack.infra.waffle.metricOptions.outboundTrafficText', { - defaultMessage: 'Outbound traffic', + defaultMessage: 'outbound traffic', }), LogRate: i18n.translate('xpack.infra.waffle.metricOptions.hostLogRateText', { - defaultMessage: 'Log rate', + defaultMessage: 'log rate', }), Load: i18n.translate('xpack.infra.waffle.metricOptions.loadText', { - defaultMessage: 'Load', + defaultMessage: 'load', }), Count: i18n.translate('xpack.infra.waffle.metricOptions.countText', { - defaultMessage: 'Count', + defaultMessage: 'count', }), DiskIOReadBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOReadBytes', { - defaultMessage: 'Disk Reads', + defaultMessage: 'disk reads', }), DiskIOWriteBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOWriteBytes', { - defaultMessage: 'Disk Writes', + defaultMessage: 'disk writes', }), s3BucketSize: i18n.translate('xpack.infra.waffle.metricOptions.s3BucketSize', { - defaultMessage: 'Bucket Size', + defaultMessage: 'bucket size', }), s3TotalRequests: i18n.translate('xpack.infra.waffle.metricOptions.s3TotalRequests', { - defaultMessage: 'Total Requests', + defaultMessage: 'total requests', }), s3NumberOfObjects: i18n.translate('xpack.infra.waffle.metricOptions.s3NumberOfObjects', { - defaultMessage: 'Number of Objects', + defaultMessage: 'number of objects', }), s3DownloadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3DownloadBytes', { - defaultMessage: 'Downloads (Bytes)', + defaultMessage: 'downloads (bytes)', }), s3UploadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3UploadBytes', { - defaultMessage: 'Uploads (Bytes)', + defaultMessage: 'uploads (bytes)', }), rdsConnections: i18n.translate('xpack.infra.waffle.metricOptions.rdsConnections', { - defaultMessage: 'Connections', + defaultMessage: 'connections', }), rdsQueriesExecuted: i18n.translate('xpack.infra.waffle.metricOptions.rdsQueriesExecuted', { - defaultMessage: 'Queries Executed', + defaultMessage: 'queries executed', }), rdsActiveTransactions: i18n.translate('xpack.infra.waffle.metricOptions.rdsActiveTransactions', { - defaultMessage: 'Active Transactions', + defaultMessage: 'active transactions', }), rdsLatency: i18n.translate('xpack.infra.waffle.metricOptions.rdsLatency', { - defaultMessage: 'Latency', + defaultMessage: 'latency', }), sqsMessagesVisible: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesVisible', { - defaultMessage: 'Messages Available', + defaultMessage: 'messages available', }), sqsMessagesDelayed: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesDelayed', { - defaultMessage: 'Messages Delayed', + defaultMessage: 'messages delayed', }), sqsMessagesSent: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesSent', { - defaultMessage: 'Messages Added', + defaultMessage: 'messages added', }), sqsMessagesEmpty: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesEmpty', { - defaultMessage: 'Messages Returned Empty', + defaultMessage: 'messages returned empty', }), sqsOldestMessage: i18n.translate('xpack.infra.waffle.metricOptions.sqsOldestMessage', { - defaultMessage: 'Oldest Message', + defaultMessage: 'oldest message', }), }; +const Translations = mapValues( + TranslationsLowercase, + (translation) => `${translation[0].toUpperCase()}${translation.slice(1)}` +); + export const toMetricOpt = ( metric: SnapshotMetricType -): { text: string; value: SnapshotMetricType } | undefined => { +): { text: string; textLC: string; value: SnapshotMetricType } | undefined => { switch (metric) { case 'cpu': return { text: Translations.CPUUsage, + textLC: TranslationsLowercase.CPUUsage, value: 'cpu', }; case 'memory': return { text: Translations.MemoryUsage, + textLC: TranslationsLowercase.MemoryUsage, value: 'memory', }; case 'rx': return { text: Translations.InboundTraffic, + textLC: TranslationsLowercase.InboundTraffic, value: 'rx', }; case 'tx': return { text: Translations.OutboundTraffic, + textLC: TranslationsLowercase.OutboundTraffic, value: 'tx', }; case 'logRate': return { text: Translations.LogRate, + textLC: TranslationsLowercase.LogRate, value: 'logRate', }; case 'load': return { text: Translations.Load, + textLC: TranslationsLowercase.Load, value: 'load', }; case 'count': return { text: Translations.Count, + textLC: TranslationsLowercase.Count, value: 'count', }; case 'diskIOReadBytes': return { text: Translations.DiskIOReadBytes, + textLC: TranslationsLowercase.DiskIOReadBytes, value: 'diskIOReadBytes', }; case 'diskIOWriteBytes': return { text: Translations.DiskIOWriteBytes, + textLC: TranslationsLowercase.DiskIOWriteBytes, value: 'diskIOWriteBytes', }; case 's3BucketSize': return { text: Translations.s3BucketSize, + textLC: TranslationsLowercase.s3BucketSize, value: 's3BucketSize', }; case 's3TotalRequests': return { text: Translations.s3TotalRequests, + textLC: TranslationsLowercase.s3TotalRequests, value: 's3TotalRequests', }; case 's3NumberOfObjects': return { text: Translations.s3NumberOfObjects, + textLC: TranslationsLowercase.s3NumberOfObjects, value: 's3NumberOfObjects', }; case 's3DownloadBytes': return { text: Translations.s3DownloadBytes, + textLC: TranslationsLowercase.s3DownloadBytes, value: 's3DownloadBytes', }; case 's3UploadBytes': return { text: Translations.s3UploadBytes, + textLC: TranslationsLowercase.s3UploadBytes, value: 's3UploadBytes', }; case 'rdsConnections': return { text: Translations.rdsConnections, + textLC: TranslationsLowercase.rdsConnections, value: 'rdsConnections', }; case 'rdsQueriesExecuted': return { text: Translations.rdsQueriesExecuted, + textLC: TranslationsLowercase.rdsQueriesExecuted, value: 'rdsQueriesExecuted', }; case 'rdsActiveTransactions': return { text: Translations.rdsActiveTransactions, + textLC: TranslationsLowercase.rdsActiveTransactions, value: 'rdsActiveTransactions', }; case 'rdsLatency': return { text: Translations.rdsLatency, + textLC: TranslationsLowercase.rdsLatency, value: 'rdsLatency', }; case 'sqsMessagesVisible': return { text: Translations.sqsMessagesVisible, + textLC: TranslationsLowercase.sqsMessagesVisible, value: 'sqsMessagesVisible', }; case 'sqsMessagesDelayed': return { text: Translations.sqsMessagesDelayed, + textLC: TranslationsLowercase.sqsMessagesDelayed, value: 'sqsMessagesDelayed', }; case 'sqsMessagesSent': return { text: Translations.sqsMessagesSent, + textLC: TranslationsLowercase.sqsMessagesSent, value: 'sqsMessagesSent', }; case 'sqsMessagesEmpty': return { text: Translations.sqsMessagesEmpty, + textLC: TranslationsLowercase.sqsMessagesEmpty, value: 'sqsMessagesEmpty', }; case 'sqsOldestMessage': return { text: Translations.sqsOldestMessage, + textLC: TranslationsLowercase.sqsOldestMessage, value: 'sqsOldestMessage', }; } diff --git a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx index 83fe233553351..698b0d3ad0caf 100644 --- a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx @@ -13,10 +13,9 @@ import { EuiDescriptionListTitle, EuiDescriptionListDescription, } from '@elastic/eui'; -import { EuiPopover } from '@elastic/eui'; +import { EuiPopover, EuiLink } from '@elastic/eui'; import { EuiListGroup, EuiListGroupItem } from '@elastic/eui'; import { EuiFlexItem } from '@elastic/eui'; -import { EuiButtonIcon } from '@elastic/eui'; import { SavedViewCreateModal } from './create_modal'; import { SavedViewUpdateModal } from './update_modal'; import { SavedViewManageViewsFlyout } from './manage_views_flyout'; @@ -151,15 +150,6 @@ export function SavedViewsToolbarControls(props: Props) { - - - (props: Props) { id="xpack.infra.savedView.currentView" /> - - {currentView - ? currentView.name - : i18n.translate('xpack.infra.savedView.unknownView', { - defaultMessage: 'No view selected', - })} - + + + {currentView + ? currentView.name + : i18n.translate('xpack.infra.savedView.unknownView', { + defaultMessage: 'No view selected', + })} + +
diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx new file mode 100644 index 0000000000000..9cb84c7fff438 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; + +import { euiStyled, useUiTracker } from '../../../../../../observability/public'; +import { InfraFormatter } from '../../../../lib/lib'; +import { Timeline } from './timeline/timeline'; + +const showHistory = i18n.translate('xpack.infra.showHistory', { + defaultMessage: 'Show history', +}); +const hideHistory = i18n.translate('xpack.infra.hideHistory', { + defaultMessage: 'Hide history', +}); + +const TRANSITION_MS = 300; + +export const BottomDrawer: React.FC<{ + measureRef: (instance: HTMLElement | null) => void; + interval: string; + formatter: InfraFormatter; +}> = ({ measureRef, interval, formatter, children }) => { + const [isOpen, setIsOpen] = useState(false); + + const trackDrawerOpen = useUiTracker({ app: 'infra_metrics' }); + const onClick = useCallback(() => { + if (!isOpen) trackDrawerOpen({ metric: 'open_timeline_drawer__inventory' }); + setIsOpen(!isOpen); + }, [isOpen, trackDrawerOpen]); + + return ( + + + + + {isOpen ? hideHistory : showHistory} + + + + {children} + + + + + + + + + + ); +}; + +const BottomActionContainer = euiStyled.div<{ isOpen: boolean }>` + padding: ${(props) => props.theme.eui.paddingSizes.m} 0; + position: fixed; + left: 0; + bottom: 0; + right: 0; + transition: transform ${TRANSITION_MS}ms; + transform: translateY(${(props) => (props.isOpen ? 0 : '224px')}) +`; + +const BottomActionTopBar = euiStyled(EuiFlexGroup).attrs({ + justifyContent: 'spaceBetween', + alignItems: 'center', +})` + margin-bottom: 0; + height: 48px; +`; + +const ShowHideButton = euiStyled(EuiButtonEmpty).attrs({ size: 's' })` + width: 140px; +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index 47616c7f4f7fd..712578be7dffd 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useEffect } from 'react'; import { useInterval } from 'react-use'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { AutoSizer } from '../../../../components/auto_sizer'; import { convertIntervalToString } from '../../../../utils/convert_interval_to_string'; import { NodesOverview } from './nodes_overview'; @@ -23,12 +23,13 @@ import { euiStyled } from '../../../../../../observability/public'; import { Toolbar } from './toolbars/toolbar'; import { ViewSwitcher } from './waffle/view_switcher'; import { IntervalLabel } from './waffle/interval_label'; -import { Legend } from './waffle/legend'; import { createInventoryMetricFormatter } from '../lib/create_inventory_metric_formatter'; import { createLegend } from '../lib/create_legend'; import { useSavedViewContext } from '../../../../containers/saved_view/saved_view'; import { useWaffleViewState } from '../hooks/use_waffle_view_state'; import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control'; +import { BottomDrawer } from './bottom_drawer'; +import { Legend } from './waffle/legend'; export const Layout = () => { const { sourceId, source } = useSourceContext(); @@ -104,12 +105,19 @@ export const Layout = () => { - + + + + + + + + {({ measureRef, bounds: { height = 0 } }) => ( @@ -128,24 +136,14 @@ export const Layout = () => { formatter={formatter} bottomMargin={height} /> - - - - - - - - - - - - - + + + )} @@ -164,12 +162,8 @@ const TopActionContainer = euiStyled.div` padding: ${(props) => `12px ${props.theme.eui.paddingSizes.m}`}; `; -const BottomActionContainer = euiStyled.div` - background-color: ${(props) => props.theme.eui.euiPageBackgroundColor}; - padding: ${(props) => props.theme.eui.paddingSizes.m} ${(props) => - props.theme.eui.paddingSizes.m}; - position: fixed; - left: 0; - bottom: 0; - right: 0; +const SavedViewContainer = euiStyled.div` + position: relative; + z-index: 1; + padding-left: ${(props) => props.theme.eui.paddingSizes.m}; `; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx new file mode 100644 index 0000000000000..2792b6eb18b00 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx @@ -0,0 +1,228 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import moment from 'moment'; +import { first, last } from 'lodash'; +import { EuiLoadingChart, EuiText, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { + Axis, + Chart, + Settings, + Position, + TooltipValue, + niceTimeFormatter, + ElementClickListener, +} from '@elastic/charts'; +import { useUiSetting } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { toMetricOpt } from '../../../../../../common/snapshot_metric_i18n'; +import { MetricsExplorerAggregation } from '../../../../../../common/http_api'; +import { Color } from '../../../../../../common/color_palette'; +import { useSourceContext } from '../../../../../containers/source'; +import { useTimeline } from '../../hooks/use_timeline'; +import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; +import { useWaffleTimeContext } from '../../hooks/use_waffle_time'; +import { useWaffleFiltersContext } from '../../hooks/use_waffle_filters'; +import { MetricExplorerSeriesChart } from '../../../metrics_explorer/components/series_chart'; +import { MetricsExplorerChartType } from '../../../metrics_explorer/hooks/use_metrics_explorer_options'; +import { getTimelineChartTheme } from '../../../metrics_explorer/components/helpers/get_chart_theme'; +import { calculateDomain } from '../../../metrics_explorer/components/helpers/calculate_domain'; + +import { euiStyled } from '../../../../../../../observability/public'; +import { InfraFormatter } from '../../../../../lib/lib'; + +interface Props { + interval: string; + yAxisFormatter: InfraFormatter; + isVisible: boolean; +} + +export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible }) => { + const { sourceId } = useSourceContext(); + const { metric, nodeType, accountId, region } = useWaffleOptionsContext(); + const { currentTime, jumpToTime, stopAutoReload } = useWaffleTimeContext(); + const { filterQueryAsJson } = useWaffleFiltersContext(); + const { loading, error, timeseries, reload } = useTimeline( + filterQueryAsJson, + [metric], + nodeType, + sourceId, + currentTime, + accountId, + region, + interval, + isVisible + ); + + const metricLabel = toMetricOpt(metric.type)?.textLC; + + const chartMetric = { + color: Color.color0, + aggregation: 'avg' as MetricsExplorerAggregation, + label: metricLabel, + }; + + const dateFormatter = useMemo(() => { + if (!timeseries) return () => ''; + const firstTimestamp = first(timeseries.rows)?.timestamp; + const lastTimestamp = last(timeseries.rows)?.timestamp; + + if (firstTimestamp == null || lastTimestamp == null) { + return (value: number) => `${value}`; + } + + return niceTimeFormatter([firstTimestamp, lastTimestamp]); + }, [timeseries]); + + const isDarkMode = useUiSetting('theme:darkMode'); + const tooltipProps = { + headerFormatter: (tooltipValue: TooltipValue) => + moment(tooltipValue.value).format('Y-MM-DD HH:mm:ss.SSS'), + }; + + const dataDomain = timeseries ? calculateDomain(timeseries, [chartMetric], false) : null; + const domain = dataDomain + ? { + max: dataDomain.max * 1.1, // add 10% headroom. + min: dataDomain.min, + } + : { max: 0, min: 0 }; + + const onClickPoint: ElementClickListener = useCallback( + ([[geometryValue]]) => { + if (!Array.isArray(geometryValue)) { + const { x: timestamp } = geometryValue; + jumpToTime(timestamp); + stopAutoReload(); + } + }, + [jumpToTime, stopAutoReload] + ); + + if (loading) { + return ( + + + + + + ); + } + + if (!loading && (error || !timeseries)) { + return ( + + {error ? errorTitle : noHistoryDataTitle}} + actions={ + + {error ? retryButtonLabel : checkNewDataButtonLabel} + + } + /> + + ); + } + + return ( + + + + + + + + + + + + + + + + + + ); +}; + +const TimelineContainer = euiStyled.div` + background-color: ${(props) => props.theme.eui.euiPageBackgroundColor}; + border-top: 1px solid ${(props) => props.theme.eui.euiColorLightShade}; + height: 220px; + width: 100%; + padding: ${(props) => props.theme.eui.paddingSizes.s} ${(props) => + props.theme.eui.paddingSizes.m}; + display: flex; + flex-direction: column; +`; + +const TimelineHeader = euiStyled.div` + display: flex; + width: 100%; + padding: ${(props) => props.theme.eui.paddingSizes.s} ${(props) => + props.theme.eui.paddingSizes.m}; +`; + +const TimelineChartContainer = euiStyled.div` + padding-left: ${(props) => props.theme.eui.paddingSizes.xs}; + width: 100%; + height: 100%; +`; + +const TimelineLoadingContainer = euiStyled.div` + display: flex; + justify-content: center; + align-items: center; + height: 100%; +`; + +const noHistoryDataTitle = i18n.translate('xpack.infra.inventoryTimeline.noHistoryDataTitle', { + defaultMessage: 'There is no history data to display.', +}); + +const errorTitle = i18n.translate('xpack.infra.inventoryTimeline.errorTitle', { + defaultMessage: 'Unable to display history data.', +}); + +const checkNewDataButtonLabel = i18n.translate( + 'xpack.infra.inventoryTimeline.checkNewDataButtonLabel', + { + defaultMessage: 'Check for new data', + } +); + +const retryButtonLabel = i18n.translate('xpack.infra.inventoryTimeline.retryButtonLabel', { + defaultMessage: 'Try again', +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx index dbbfb0f49c0e9..6e031c8396f07 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx @@ -22,7 +22,7 @@ export const IntervalLabel = ({ intervalAsString }: Props) => {

diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts index 06b53d531f53c..702213516c123 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts @@ -43,7 +43,7 @@ export function useSnapshot( const timerange: InfraTimerangeInput = { interval: '1m', to: currentTime, - from: currentTime - 360 * 1000, + from: currentTime - 1200 * 1000, lookbackSize: 20, }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_timeline.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_timeline.ts new file mode 100644 index 0000000000000..650eda0362d9e --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_timeline.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { first } from 'lodash'; +import { useEffect, useMemo, useCallback } from 'react'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getIntervalInSeconds } from '../../../../../server/utils/get_interval_in_seconds'; +import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; +import { useHTTPRequest } from '../../../../hooks/use_http_request'; +import { + SnapshotNodeResponseRT, + SnapshotNodeResponse, + SnapshotRequest, + InfraTimerangeInput, +} from '../../../../../common/http_api/snapshot_api'; +import { + InventoryItemType, + SnapshotMetricType, +} from '../../../../../common/inventory_models/types'; + +const ONE_MINUTE = 60; +const ONE_HOUR = ONE_MINUTE * 60; +const ONE_DAY = ONE_HOUR * 24; +const ONE_WEEK = ONE_DAY * 7; + +const getTimeLengthFromInterval = (interval: string | undefined) => { + if (interval) { + const intervalInSeconds = getIntervalInSeconds(interval); + const multiplier = + intervalInSeconds < ONE_MINUTE + ? ONE_HOUR / intervalInSeconds + : intervalInSeconds < ONE_HOUR + ? 60 + : intervalInSeconds < ONE_DAY + ? 7 + : intervalInSeconds < ONE_WEEK + ? 30 + : 1; + const timeLength = intervalInSeconds * multiplier; + return { timeLength, intervalInSeconds }; + } else { + return { timeLength: 0, intervalInSeconds: 0 }; + } +}; + +export function useTimeline( + filterQuery: string | null | undefined, + metrics: Array<{ type: SnapshotMetricType }>, + nodeType: InventoryItemType, + sourceId: string, + currentTime: number, + accountId: string, + region: string, + interval: string | undefined, + shouldReload: boolean +) { + const decodeResponse = (response: any) => { + return pipe( + SnapshotNodeResponseRT.decode(response), + fold(throwErrors(createPlainError), identity) + ); + }; + + const timeLengthResult = useMemo(() => getTimeLengthFromInterval(interval), [interval]); + const { timeLength, intervalInSeconds } = timeLengthResult; + + const timerange: InfraTimerangeInput = { + interval: interval ?? '', + to: currentTime + intervalInSeconds * 1000, + from: currentTime - timeLength * 1000, + lookbackSize: 0, + ignoreLookback: true, + }; + + const { error, loading, response, makeRequest } = useHTTPRequest( + '/api/metrics/snapshot', + 'POST', + JSON.stringify({ + metrics, + groupBy: null, + nodeType, + timerange, + filterQuery, + sourceId, + accountId, + region, + includeTimeseries: true, + } as SnapshotRequest), + decodeResponse + ); + + const loadData = useCallback(() => { + if (shouldReload) return makeRequest(); + return Promise.resolve(); + }, [makeRequest, shouldReload]); + + useEffect(() => { + (async () => { + if (timeLength) { + await loadData(); + } + })(); + }, [loadData, timeLength]); + + const timeseries = response + ? first(response.nodes.map((node) => first(node.metrics)?.timeseries)) + : null; + + return { + error: (error && error.message) || null, + loading: !interval ? true : loading, + timeseries, + reload: makeRequest, + }; +} diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_chart_theme.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_chart_theme.ts index 42469ffb5ee9a..bb6a70f65bb97 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_chart_theme.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_chart_theme.ts @@ -4,8 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Theme, LIGHT_THEME, DARK_THEME } from '@elastic/charts'; +import { + Theme, + PartialTheme, + LIGHT_THEME, + DARK_THEME, + mergeWithDefaultTheme, +} from '@elastic/charts'; export function getChartTheme(isDarkMode: boolean): Theme { return isDarkMode ? DARK_THEME : LIGHT_THEME; } + +export function getTimelineChartTheme(isDarkMode: boolean): Theme { + return isDarkMode ? DARK_THEME : mergeWithDefaultTheme(TIMELINE_LIGHT_THEME, LIGHT_THEME); +} + +const TIMELINE_LIGHT_THEME: PartialTheme = { + crosshair: { + band: { + fill: '#D3DAE6', + }, + }, + axes: { + gridLine: { + horizontal: { + stroke: '#eaeaea', + }, + }, + }, +}; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts index 700f4ef39bb66..814ec5e74ff33 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts @@ -56,8 +56,10 @@ export const transformRequestToMetricsAPIRequest = async ( snapshotRequest.nodeType, source.configuration.fields ); - const groupBy = snapshotRequest.groupBy.map((g) => g.field).filter(Boolean) as string[]; - metricsApiRequest.groupBy = [...groupBy, inventoryFields.id]; + if (snapshotRequest.groupBy) { + const groupBy = snapshotRequest.groupBy.map((g) => g.field).filter(Boolean) as string[]; + metricsApiRequest.groupBy = [...groupBy, inventoryFields.id]; + } const metaAggregation = { id: META_KEY, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7ca4e02068d41..9668bdf4e6781 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8815,7 +8815,6 @@ "xpack.infra.registerFeatures.logsDescription": "ログをリアルタイムでストリーするか、コンソール式の UI で履歴ビューをスクロールします。", "xpack.infra.registerFeatures.logsTitle": "ログ", "xpack.infra.sampleDataLinkLabel": "ログ", - "xpack.infra.savedView.changeView": "ビューの変更", "xpack.infra.savedView.currentView": "現在のビュー", "xpack.infra.savedView.defaultViewNameHosts": "デフォルトビュー", "xpack.infra.savedView.errorOnCreate.duplicateViewName": "その名前のビューは既に存在します", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2e1fb55777cdf..f946709f7c23b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8821,7 +8821,6 @@ "xpack.infra.registerFeatures.logsDescription": "实时流式传输日志或在类似控制台的工具中滚动浏览历史视图。", "xpack.infra.registerFeatures.logsTitle": "日志", "xpack.infra.sampleDataLinkLabel": "日志", - "xpack.infra.savedView.changeView": "更改视图", "xpack.infra.savedView.currentView": "当前视图", "xpack.infra.savedView.defaultViewNameHosts": "默认视图", "xpack.infra.savedView.errorOnCreate.duplicateViewName": "具有该名称的视图已存在。", From 48b81a62f7e530fe8a7d057f163a1020fe396bfc Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Tue, 22 Sep 2020 14:23:54 -0700 Subject: [PATCH 23/92] [Ingest Manager] Agent bulk actions UI (#77690) * Add temporary client-side license service/hook * Initial pass at bulk actions UI (UI behavior only) * Initial pass at implementing reassign agent policy by agent IDs * Allow bulk reassign agent policy by kuery * Return total inactive agents in list agents API to better handle bulk action selection UI that may or may not include active agents * Add isGoldPlus method to license service * Add `normalizeKuery` function * Add `.findAllSOs` method and refactor bulk reassign to use that * Initial pass at backend work for bulk unenroll * Covert unenroll provider to unenroll modal and adjust UI to include force option * Move license protection to handler level, fix misc bugs * Add comments about `data` field response in create agent action(s) * Clean up license service * Fix i18n * Add tests for bulk. unenroll * Add tests for reassign and bulk reassign * Fix typing * Adjust single actions icon and text to be consistent * Fix i18n * PR feedback * Increment api key test assertion to account for adding another agent policy to es archiver data * Fix test * Fix duplicate declaration after merging * Add comments to SO find all function * Batch invalidate API keys requests --- .../ingest_manager/common/constants/routes.ts | 2 + .../ingest_manager/common/services/index.ts | 1 + .../ingest_manager/common/services/license.ts | 46 ++++ .../ingest_manager/common/services/routes.ts | 2 + .../common/types/rest_spec/agent.ts | 28 +++ .../ingest_manager/hooks/index.ts | 1 + .../ingest_manager/hooks/use_license.ts | 12 + .../hooks/use_request/agents.ts | 43 ++++ .../applications/ingest_manager/index.tsx | 12 +- .../components/actions_menu.tsx | 60 +++-- .../components/bulk_actions.tsx | 225 ++++++++++++++++++ .../sections/fleet/agent_list_page/index.tsx | 203 ++++++++++------ .../agent_reassign_policy_flyout/index.tsx | 43 +++- .../components/agent_unenroll_modal/index.tsx | 166 +++++++++++++ .../components/agent_unenroll_provider.tsx | 174 -------------- .../sections/fleet/components/index.tsx | 2 +- .../ingest_manager/services/index.ts | 1 + .../ingest_manager/types/index.ts | 5 + .../plugins/ingest_manager/public/plugin.ts | 5 +- .../server/routes/agent/handlers.ts | 52 +++- .../server/routes/agent/index.ts | 27 ++- .../server/routes/agent/unenroll_handler.ts | 36 ++- .../server/services/agent_policy.ts | 9 +- .../server/services/agents/actions.ts | 70 +++++- .../server/services/agents/crud.ts | 128 +++++++--- .../server/services/agents/events.ts | 8 +- .../server/services/agents/reassign.ts | 42 ++++ .../server/services/agents/unenroll.ts | 112 ++++++++- .../services/api_keys/enrollment_api_key.ts | 6 +- .../services/epm/registry/registry_url.ts | 3 +- .../ingest_manager/server/services/index.ts | 18 +- .../ingest_manager/server/services/license.ts | 32 +-- .../server/services/package_policy.ts | 9 +- .../server/services/saved_object.ts | 69 ++++++ .../server/types/rest_spec/agent.ts | 14 ++ .../translations/translations/ja-JP.json | 10 +- .../translations/translations/zh-CN.json | 10 +- .../es_archives/fleet/agents/data.json | 32 +++ .../apis/fleet/agents/reassign.ts | 94 ++++++++ .../apis/fleet/agents/unenroll.ts | 36 ++- .../apis/fleet/enrollment_api_keys/crud.ts | 2 +- .../apis/fleet/index.js | 1 + 42 files changed, 1416 insertions(+), 435 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/common/services/license.ts create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_license.ts create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_modal/index.tsx delete mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fleet/agents/reassign.ts diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index 378a6c6c12159..d899739a74ef0 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -86,7 +86,9 @@ export const AGENT_API_ROUTES = { ACTIONS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/actions`, ENROLL_PATTERN: `${FLEET_API_ROOT}/agents/enroll`, UNENROLL_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/unenroll`, + BULK_UNENROLL_PATTERN: `${FLEET_API_ROOT}/agents/bulk_unenroll`, REASSIGN_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/reassign`, + BULK_REASSIGN_PATTERN: `${FLEET_API_ROOT}/agents/bulk_reassign`, STATUS_PATTERN: `${FLEET_API_ROOT}/agent-status`, }; diff --git a/x-pack/plugins/ingest_manager/common/services/index.ts b/x-pack/plugins/ingest_manager/common/services/index.ts index 46a1c65872d1b..4bffa01ad5ee2 100644 --- a/x-pack/plugins/ingest_manager/common/services/index.ts +++ b/x-pack/plugins/ingest_manager/common/services/index.ts @@ -12,3 +12,4 @@ export { isPackageLimited, doesAgentPolicyAlreadyIncludePackage } from './limite export { decodeCloudId } from './decode_cloud_id'; export { isValidNamespace } from './is_valid_namespace'; export { isDiffPathProtocol } from './is_diff_path_protocol'; +export { LicenseService } from './license'; diff --git a/x-pack/plugins/ingest_manager/common/services/license.ts b/x-pack/plugins/ingest_manager/common/services/license.ts new file mode 100644 index 0000000000000..6d9b20a8456c0 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/services/license.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Observable, Subscription } from 'rxjs'; +import { ILicense } from '../../../licensing/common/types'; + +// Generic license service class that works with the license observable +// Both server and client plugins instancates a singleton version of this class +export class LicenseService { + private observable: Observable | null = null; + private subscription: Subscription | null = null; + private licenseInformation: ILicense | null = null; + + private updateInformation(licenseInformation: ILicense) { + this.licenseInformation = licenseInformation; + } + + public start(license$: Observable) { + this.observable = license$; + this.subscription = this.observable.subscribe(this.updateInformation.bind(this)); + } + + public stop() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public getLicenseInformation() { + return this.licenseInformation; + } + + public getLicenseInformation$() { + return this.observable; + } + + public isGoldPlus() { + return ( + this.licenseInformation?.isAvailable && + this.licenseInformation?.isActive && + this.licenseInformation?.hasAtLeast('gold') + ); + } +} diff --git a/x-pack/plugins/ingest_manager/common/services/routes.ts b/x-pack/plugins/ingest_manager/common/services/routes.ts index ec7c0ee850834..3c3534926908a 100644 --- a/x-pack/plugins/ingest_manager/common/services/routes.ts +++ b/x-pack/plugins/ingest_manager/common/services/routes.ts @@ -131,8 +131,10 @@ export const agentRouteService = { getEventsPath: (agentId: string) => AGENT_API_ROUTES.EVENTS_PATTERN.replace('{agentId}', agentId), getUnenrollPath: (agentId: string) => AGENT_API_ROUTES.UNENROLL_PATTERN.replace('{agentId}', agentId), + getBulkUnenrollPath: () => AGENT_API_ROUTES.BULK_UNENROLL_PATTERN, getReassignPath: (agentId: string) => AGENT_API_ROUTES.REASSIGN_PATTERN.replace('{agentId}', agentId), + getBulkReassignPath: () => AGENT_API_ROUTES.BULK_REASSIGN_PATTERN, getListPath: () => AGENT_API_ROUTES.LIST_PATTERN, getStatusPath: () => AGENT_API_ROUTES.STATUS_PATTERN, }; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts index 54cdeade3764e..1a10d4930656f 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts @@ -26,6 +26,7 @@ export interface GetAgentsRequest { export interface GetAgentsResponse { list: Agent[]; total: number; + totalInactive: number; page: number; perPage: number; } @@ -104,11 +105,24 @@ export interface PostAgentUnenrollRequest { params: { agentId: string; }; + body: { + force?: boolean; + }; } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PostAgentUnenrollResponse {} +export interface PostBulkAgentUnenrollRequest { + body: { + agents: string[] | string; + force?: boolean; + }; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PostBulkAgentUnenrollResponse {} + export interface PutAgentReassignRequest { params: { agentId: string; @@ -119,6 +133,20 @@ export interface PutAgentReassignRequest { // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PutAgentReassignResponse {} +export interface PostBulkAgentReassignRequest { + body: { + policy_id: string; + agents: string[] | string; + }; +} + +export interface PostBulkAgentReassignResponse { + [key: string]: { + success: boolean; + error?: Error; + }; +} + export interface GetOneAgentEventsRequest { params: { agentId: string; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts index 36b7d412bf276..64434e163f043 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts @@ -8,6 +8,7 @@ export { useCapabilities } from './use_capabilities'; export { useCore } from './use_core'; export { useConfig, ConfigContext } from './use_config'; export { useSetupDeps, useStartDeps, DepsContext } from './use_deps'; +export { licenseService, useLicense } from './use_license'; export { useBreadcrumbs } from './use_breadcrumbs'; export { useLink } from './use_link'; export { useKibanaLink } from './use_kibana_link'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_license.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_license.ts new file mode 100644 index 0000000000000..411a6d6f2168f --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_license.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { LicenseService } from '../services'; + +export const licenseService = new LicenseService(); + +export function useLicense() { + return licenseService; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts index cad1791af41be..41967fd068e0b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts @@ -10,8 +10,14 @@ import { GetOneAgentResponse, GetOneAgentEventsResponse, GetOneAgentEventsRequest, + PostAgentUnenrollRequest, + PostBulkAgentUnenrollRequest, + PostBulkAgentUnenrollResponse, + PostAgentUnenrollResponse, PutAgentReassignRequest, PutAgentReassignResponse, + PostBulkAgentReassignRequest, + PostBulkAgentReassignResponse, GetAgentsRequest, GetAgentsResponse, GetAgentStatusRequest, @@ -83,3 +89,40 @@ export function sendPutAgentReassign( ...options, }); } + +export function sendPostBulkAgentReassign( + body: PostBulkAgentReassignRequest['body'], + options?: RequestOptions +) { + return sendRequest({ + method: 'post', + path: agentRouteService.getBulkReassignPath(), + body, + ...options, + }); +} + +export function sendPostAgentUnenroll( + agentId: string, + body: PostAgentUnenrollRequest['body'], + options?: RequestOptions +) { + return sendRequest({ + path: agentRouteService.getUnenrollPath(agentId), + method: 'post', + body, + ...options, + }); +} + +export function sendPostBulkAgentUnenroll( + body: PostBulkAgentUnenrollRequest['body'], + options?: RequestOptions +) { + return sendRequest({ + path: agentRouteService.getBulkUnenrollPath(), + method: 'post', + body, + ...options, + }); +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx index 5520a50463db4..45ac538d9e394 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -22,9 +22,16 @@ import { PAGE_ROUTING_PATHS } from './constants'; import { DefaultLayout, WithoutHeaderLayout } from './layouts'; import { Loading, Error } from './components'; import { IngestManagerOverview, EPMApp, AgentPolicyApp, FleetApp, DataStreamApp } from './sections'; -import { DepsContext, ConfigContext, useConfig } from './hooks'; +import { + DepsContext, + ConfigContext, + useConfig, + useCore, + sendSetup, + sendGetPermissionsCheck, + licenseService, +} from './hooks'; import { PackageInstallProvider } from './sections/epm/hooks'; -import { useCore, sendSetup, sendGetPermissionsCheck } from './hooks'; import { FleetStatusProvider } from './hooks/use_fleet_status'; import './index.scss'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; @@ -279,4 +286,5 @@ export function renderApp( export const teardownIngestManager = (coreStart: CoreStart) => { coreStart.chrome.docTitle.reset(); coreStart.chrome.setBreadcrumbs([]); + licenseService.stop(); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/actions_menu.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/actions_menu.tsx index 636ff7a5ff989..ea5dcce8c05bb 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/actions_menu.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/actions_menu.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Agent } from '../../../../types'; import { useCapabilities } from '../../../../hooks'; import { ContextMenuActions } from '../../../../components'; -import { AgentUnenrollProvider, AgentReassignAgentPolicyFlyout } from '../../components'; +import { AgentUnenrollAgentModal, AgentReassignAgentPolicyFlyout } from '../../components'; import { useAgentRefresh } from '../hooks'; export const AgentDetailsActionMenu: React.FunctionComponent<{ @@ -20,6 +20,7 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{ const hasWriteCapabilites = useCapabilities().write; const refreshAgent = useAgentRefresh(); const [isReassignFlyoutOpen, setIsReassignFlyoutOpen] = useState(assignFlyoutOpenByDefault); + const [isUnenrollModalOpen, setIsUnenrollModalOpen] = useState(false); const isUnenrolling = agent.status === 'unenrolling'; const onClose = useMemo(() => { @@ -34,7 +35,20 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{ <> {isReassignFlyoutOpen && ( - + + + )} + {isUnenrollModalOpen && ( + + { + setIsUnenrollModalOpen(false); + refreshAgent(); + }} + useForceUnenroll={isUnenrolling} + /> )} , - - {(unenrollAgentsPrompt) => ( - { - unenrollAgentsPrompt([agent.id], 1, refreshAgent); - }} - > - {isUnenrolling ? ( - - ) : ( - - )} - + { + setIsUnenrollModalOpen(true); + }} + > + {isUnenrolling ? ( + + ) : ( + )} - , + , ]} /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx new file mode 100644 index 0000000000000..25684c9faf594 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx @@ -0,0 +1,225 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiPopover, + EuiContextMenu, + EuiButtonEmpty, + EuiIcon, + EuiPortal, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Agent } from '../../../../types'; +import { AgentReassignAgentPolicyFlyout, AgentUnenrollAgentModal } from '../../components'; + +const Divider = styled.div` + width: 0; + height: ${(props) => props.theme.eui.euiSizeL}; + border-left: ${(props) => props.theme.eui.euiBorderThin}; +`; + +const FlexItem = styled(EuiFlexItem)` + height: ${(props) => props.theme.eui.euiSizeL}; +`; + +const Button = styled(EuiButtonEmpty)` + .euiButtonEmpty__text { + font-size: ${(props) => props.theme.eui.euiFontSizeXS}; + } +`; + +export type SelectionMode = 'manual' | 'query'; + +export const AgentBulkActions: React.FunctionComponent<{ + totalAgents: number; + totalInactiveAgents: number; + selectableAgents: number; + selectionMode: SelectionMode; + setSelectionMode: (mode: SelectionMode) => void; + currentQuery: string; + selectedAgents: Agent[]; + setSelectedAgents: (agents: Agent[]) => void; + refreshAgents: () => void; +}> = ({ + totalAgents, + totalInactiveAgents, + selectableAgents, + selectionMode, + setSelectionMode, + currentQuery, + selectedAgents, + setSelectedAgents, + refreshAgents, +}) => { + // Bulk actions menu states + const [isMenuOpen, setIsMenuOpen] = useState(false); + const closeMenu = () => setIsMenuOpen(false); + const openMenu = () => setIsMenuOpen(true); + + // Actions states + const [isReassignFlyoutOpen, setIsReassignFlyoutOpen] = useState(false); + const [isUnenrollModalOpen, setIsUnenrollModalOpen] = useState(false); + + // Check if user is working with only inactive agents + const atLeastOneActiveAgentSelected = + selectionMode === 'manual' + ? !!selectedAgents.find((agent) => agent.active) + : totalAgents > totalInactiveAgents; + + const panels = [ + { + id: 0, + items: [ + { + name: ( + + ), + icon: , + disabled: !atLeastOneActiveAgentSelected, + onClick: () => { + closeMenu(); + setIsReassignFlyoutOpen(true); + }, + }, + { + name: ( + + ), + icon: , + disabled: !atLeastOneActiveAgentSelected, + onClick: () => { + closeMenu(); + setIsUnenrollModalOpen(true); + }, + }, + { + name: ( + + ), + icon: , + onClick: () => { + closeMenu(); + setSelectionMode('manual'); + setSelectedAgents([]); + }, + }, + ], + }, + ]; + + return ( + <> + {isReassignFlyoutOpen && ( + + { + setIsReassignFlyoutOpen(false); + refreshAgents(); + }} + /> + + )} + {isUnenrollModalOpen && ( + + { + setIsUnenrollModalOpen(false); + refreshAgents(); + }} + /> + + )} + + + + + + + {(selectionMode === 'manual' && selectedAgents.length) || + (selectionMode === 'query' && totalAgents > 0) ? ( + <> + + + + + + + + } + isOpen={isMenuOpen} + closePopover={closeMenu} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + + {selectionMode === 'manual' && + selectedAgents.length === selectableAgents && + selectableAgents < totalAgents ? ( + + + + ) : null} + + ) : ( + + )} + + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index 46f7ffb85b21f..0bc463ce98590 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useMemo, useCallback } from 'react'; +import React, { useState, useMemo, useCallback, useRef } from 'react'; import { EuiBasicTable, EuiButton, @@ -20,6 +20,7 @@ import { EuiContextMenuItem, EuiIcon, EuiPortal, + EuiHorizontalRule, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; @@ -33,11 +34,17 @@ import { useUrlParams, useLink, useBreadcrumbs, + useLicense, } from '../../../hooks'; import { SearchBar, ContextMenuActions } from '../../../components'; import { AgentStatusKueryHelper } from '../../../services'; import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; -import { AgentReassignAgentPolicyFlyout, AgentHealth, AgentUnenrollProvider } from '../components'; +import { + AgentReassignAgentPolicyFlyout, + AgentHealth, + AgentUnenrollAgentModal, +} from '../components'; +import { AgentBulkActions, SelectionMode } from './components/bulk_actions'; const REFRESH_INTERVAL_MS = 5000; @@ -63,72 +70,68 @@ const statusFilters = [ }, ] as Array<{ label: string; status: string }>; -const RowActions = React.memo<{ agent: Agent; onReassignClick: () => void; refresh: () => void }>( - ({ agent, refresh, onReassignClick }) => { - const { getHref } = useLink(); - const hasWriteCapabilites = useCapabilities().write; +const RowActions = React.memo<{ + agent: Agent; + refresh: () => void; + onReassignClick: () => void; + onUnenrollClick: () => void; +}>(({ agent, refresh, onReassignClick, onUnenrollClick }) => { + const { getHref } = useLink(); + const hasWriteCapabilites = useCapabilities().write; - const isUnenrolling = agent.status === 'unenrolling'; - const [isMenuOpen, setIsMenuOpen] = useState(false); - return ( - setIsMenuOpen(isOpen)} - items={[ - + const isUnenrolling = agent.status === 'unenrolling'; + const [isMenuOpen, setIsMenuOpen] = useState(false); + return ( + setIsMenuOpen(isOpen)} + items={[ + + + , + { + onReassignClick(); + }} + disabled={!agent.active} + key="reassignPolicy" + > + + , + { + onUnenrollClick(); + }} + > + {isUnenrolling ? ( - , - { - onReassignClick(); - }} - disabled={!agent.active} - key="reassignPolicy" - > + ) : ( - , - - {(unenrollAgentsPrompt) => ( - { - unenrollAgentsPrompt([agent.id], 1, () => { - refresh(); - setIsMenuOpen(false); - }); - }} - > - {isUnenrolling ? ( - - ) : ( - - )} - - )} - , - ]} - /> - ); - } -); + )} + , + ]} + /> + ); +}); function safeMetadata(val: any) { if (typeof val !== 'string') { @@ -142,12 +145,16 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const { getHref } = useLink(); const defaultKuery: string = (useUrlParams().urlParams.kuery as string) || ''; const hasWriteCapabilites = useCapabilities().write; + const isGoldPlus = useLicense().isGoldPlus(); // Agent data states const [showInactive, setShowInactive] = useState(false); // Table and search states - const [search, setSearch] = useState(defaultKuery); + const [search, setSearch] = useState(defaultKuery); + const [selectionMode, setSelectionMode] = useState('manual'); + const [selectedAgents, setSelectedAgents] = useState([]); + const tableRef = useRef>(null); const { pagination, pageSizeOptions, setPagination } = usePagination(); // Policies state for filtering @@ -179,8 +186,9 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { // Agent enrollment flyout state const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); - // Agent reassignment flyout state - const [agentToReassignId, setAgentToReassignId] = useState(undefined); + // Agent actions states + const [agentToReassign, setAgentToReassign] = useState(undefined); + const [agentToUnenroll, setAgentToUnenroll] = useState(undefined); let kuery = search.trim(); if (selectedAgentPolicies.length) { @@ -229,6 +237,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const agents = agentsRequest.data ? agentsRequest.data.list : []; const totalAgents = agentsRequest.data ? agentsRequest.data.total : 0; + const totalInactiveAgents = agentsRequest.data ? agentsRequest.data.totalInactive : 0; const { isLoading } = agentsRequest; const agentPoliciesRequest = useGetAgentPolicies({ @@ -345,7 +354,8 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { agentsRequest.resendRequest()} - onReassignClick={() => setAgentToReassignId(agent.id)} + onReassignClick={() => setAgentToReassign(agent)} + onUnenrollClick={() => setAgentToUnenroll(agent)} /> ); }, @@ -378,8 +388,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { /> ); - const agentToReassign = agentToReassignId && agents.find((a) => a.id === agentToReassignId); - return ( <> {isEnrollmentFlyoutOpen ? ( @@ -391,15 +399,30 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { {agentToReassign && ( { - setAgentToReassignId(undefined); + setAgentToReassign(undefined); agentsRequest.resendRequest(); }} /> )} - + {agentToUnenroll && ( + + { + setAgentToUnenroll(undefined); + agentsRequest.resendRequest(); + }} + useForceUnenroll={agentToUnenroll.status === 'unenrolling'} + /> + + )} + + {/* Search and filter bar */} + @@ -510,9 +533,31 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { - + + {/* Agent total and bulk actions */} + agent.active).length || 0} + selectionMode={selectionMode} + setSelectionMode={setSelectionMode} + currentQuery={kuery} + selectedAgents={selectedAgents} + setSelectedAgents={(newAgents: Agent[]) => { + if (tableRef?.current) { + tableRef.current.setSelection(newAgents); + setSelectionMode('manual'); + } + }} + refreshAgents={() => agentsRequest.resendRequest()} + /> + + + + {/* Agent list table */} + ref={tableRef} className="fleet__agentList__table" data-test-subj="fleetAgentListTable" loading={isLoading} @@ -551,6 +596,18 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { totalItemCount: totalAgents, pageSizeOptions, }} + isSelectable={true} + selection={ + isGoldPlus + ? { + onSelectionChange: (newAgents: Agent[]) => { + setSelectedAgents(newAgents); + setSelectionMode('manual'); + }, + selectable: (agent: Agent) => agent.active, + } + : undefined + } onChange={({ page }: { page: { index: number; size: number } }) => { const newPagination = { ...pagination, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_policy_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_policy_flyout/index.tsx index 0c154bf1074c0..d3af1287c4025 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_policy_flyout/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_policy_flyout/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlyout, @@ -22,40 +22,55 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Agent } from '../../../../types'; -import { sendPutAgentReassign, useCore, useGetAgentPolicies } from '../../../../hooks'; +import { + sendPutAgentReassign, + sendPostBulkAgentReassign, + useCore, + useGetAgentPolicies, +} from '../../../../hooks'; import { AgentPolicyPackageBadges } from '../agent_policy_package_badges'; interface Props { onClose: () => void; - agent: Agent; + agents: Agent[] | string; } export const AgentReassignAgentPolicyFlyout: React.FunctionComponent = ({ onClose, - agent, + agents, }) => { const { notifications } = useCore(); + const isSingleAgent = Array.isArray(agents) && agents.length === 1; + const [selectedAgentPolicyId, setSelectedAgentPolicyId] = useState( - agent.policy_id + isSingleAgent ? (agents[0] as Agent).policy_id : undefined ); - const agentPoliciesRequest = useGetAgentPolicies({ page: 1, perPage: 1000, }); const agentPolicies = agentPoliciesRequest.data ? agentPoliciesRequest.data.items : []; + useEffect(() => { + if (!selectedAgentPolicyId && agentPolicies[0]) { + setSelectedAgentPolicyId(agentPolicies[0].id); + } + }, [agentPolicies, selectedAgentPolicyId]); const [isSubmitting, setIsSubmitting] = useState(false); - async function onSubmit() { try { setIsSubmitting(true); if (!selectedAgentPolicyId) { throw new Error('No selected agent policy id'); } - const res = await sendPutAgentReassign(agent.id, { - policy_id: selectedAgentPolicyId, - }); + const res = isSingleAgent + ? await sendPutAgentReassign((agents[0] as Agent).id, { + policy_id: selectedAgentPolicyId, + }) + : await sendPostBulkAgentReassign({ + policy_id: selectedAgentPolicyId, + agents: Array.isArray(agents) ? agents.map((agent) => agent.id) : agents, + }); if (res.error) { throw res.error; } @@ -91,7 +106,10 @@ export const AgentReassignAgentPolicyFlyout: React.FunctionComponent = ({ @@ -106,6 +124,7 @@ export const AgentReassignAgentPolicyFlyout: React.FunctionComponent = ({ > ({ value: agentPolicy.id, text: agentPolicy.name, @@ -134,7 +153,7 @@ export const AgentReassignAgentPolicyFlyout: React.FunctionComponent = ({ void; + agents: Agent[] | string; + agentCount: number; + useForceUnenroll?: boolean; +} + +export const AgentUnenrollAgentModal: React.FunctionComponent = ({ + onClose, + agents, + agentCount, + useForceUnenroll, +}) => { + const { notifications } = useCore(); + const [forceUnenroll, setForceUnenroll] = useState(useForceUnenroll || false); + const [isSubmitting, setIsSubmitting] = useState(false); + const isSingleAgent = Array.isArray(agents) && agents.length === 1; + + async function onSubmit() { + try { + setIsSubmitting(true); + const { error } = isSingleAgent + ? await sendPostAgentUnenroll((agents[0] as Agent).id, { + force: forceUnenroll, + }) + : await sendPostBulkAgentUnenroll({ + agents: Array.isArray(agents) ? agents.map((agent) => agent.id) : agents, + force: forceUnenroll, + }); + if (error) { + throw error; + } + setIsSubmitting(false); + if (forceUnenroll) { + const successMessage = isSingleAgent + ? i18n.translate( + 'xpack.ingestManager.unenrollAgents.successForceSingleNotificationTitle', + { defaultMessage: 'Agent unenrolled' } + ) + : i18n.translate( + 'xpack.ingestManager.unenrollAgents.successForceMultiNotificationTitle', + { defaultMessage: 'Agents unenrolled' } + ); + notifications.toasts.addSuccess(successMessage); + } else { + const successMessage = isSingleAgent + ? i18n.translate('xpack.ingestManager.unenrollAgents.successSingleNotificationTitle', { + defaultMessage: 'Unenrolling agent', + }) + : i18n.translate('xpack.ingestManager.unenrollAgents.successMultiNotificationTitle', { + defaultMessage: 'Unenrolling agents', + }); + notifications.toasts.addSuccess(successMessage); + } + onClose(); + } catch (error) { + setIsSubmitting(false); + notifications.toasts.addError(error, { + title: i18n.translate('xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle', { + defaultMessage: 'Error unenrolling {count, plural, one {agent} other {agents}}', + values: { count: agentCount }, + }), + }); + } + } + + return ( + + + ) : ( + + ) + } + onCancel={onClose} + onConfirm={onSubmit} + cancelButtonText={ + + } + confirmButtonDisabled={isSubmitting} + confirmButtonText={ + isSingleAgent ? ( + + ) : ( + + ) + } + buttonColor="danger" + > +

+ {isSingleAgent ? ( + + ) : ( + + )} +

+ + ), + }} + > + + } + checked={forceUnenroll} + onChange={(e) => setForceUnenroll(e.target.checked)} + disabled={useForceUnenroll} + /> + +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx deleted file mode 100644 index 6f1cba70bbcee..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx +++ /dev/null @@ -1,174 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment, useRef, useState } from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useCore, sendRequest } from '../../../hooks'; -import { PostAgentUnenrollResponse } from '../../../types'; -import { agentRouteService } from '../../../services'; - -interface Props { - children: (unenrollAgents: UnenrollAgents) => React.ReactElement; - forceUnenroll?: boolean; -} - -export type UnenrollAgents = ( - agents: string[] | string, - agentsCount: number, - onSuccess?: OnSuccessCallback -) => void; - -type OnSuccessCallback = (agentsUnenrolled: string[]) => void; - -export const AgentUnenrollProvider: React.FunctionComponent = ({ - children, - forceUnenroll = false, -}) => { - const core = useCore(); - const [agents, setAgents] = useState([]); - const [agentsCount, setAgentsCount] = useState(0); - const [isModalOpen, setIsModalOpen] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const onSuccessCallback = useRef(null); - - const unenrollAgentsPrompt: UnenrollAgents = ( - agentsToUnenroll, - agentsToUnenrollCount, - onSuccess = () => undefined - ) => { - if ( - agentsToUnenroll === undefined || - // !Only supports unenrolling one agent - (Array.isArray(agentsToUnenroll) && agentsToUnenroll.length !== 1) - ) { - throw new Error('No agents specified for unenrollment'); - } - setIsModalOpen(true); - setAgents(agentsToUnenroll); - setAgentsCount(agentsToUnenrollCount); - onSuccessCallback.current = onSuccess; - }; - - const closeModal = () => { - setAgents([]); - setAgentsCount(0); - setIsLoading(false); - setIsModalOpen(false); - }; - - const unenrollAgents = async () => { - setIsLoading(true); - - try { - const agentId = agents[0]; - const { error } = await sendRequest({ - path: agentRouteService.getUnenrollPath(agentId), - method: 'post', - body: { - force: forceUnenroll, - }, - }); - - if (error) { - throw new Error(error.message); - } - - const successMessage = forceUnenroll - ? i18n.translate('xpack.ingestManager.unenrollAgents.successForceSingleNotificationTitle', { - defaultMessage: "Agent '{id}' unenrolled", - values: { id: agentId }, - }) - : i18n.translate('xpack.ingestManager.unenrollAgents.successSingleNotificationTitle', { - defaultMessage: "Unenrolling agent '{id}'", - values: { id: agentId }, - }); - core.notifications.toasts.addSuccess(successMessage); - - if (onSuccessCallback.current) { - onSuccessCallback.current([agentId]); - } - } catch (e) { - core.notifications.toasts.addDanger( - i18n.translate('xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle', { - defaultMessage: 'Error unenrolling agents', - }) - ); - } - - closeModal(); - }; - - const renderModal = () => { - if (!isModalOpen) { - return null; - } - - const unenrollByKuery = typeof agents === 'string'; - const isSingle = agentsCount === 1; - - return ( - - - ) : ( - - ) - ) : ( - - ) - } - onCancel={closeModal} - onConfirm={unenrollAgents} - cancelButtonText={ - - } - confirmButtonText={ - isLoading ? ( - - ) : ( - - ) - } - buttonColor="danger" - confirmButtonDisabled={isLoading} - /> - - ); - }; - - return ( - - {children(unenrollAgentsPrompt)} - {renderModal()} - - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx index 527f920f24365..eea4ed3b712b1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx @@ -8,4 +8,4 @@ export * from './loading'; export * from './agent_reassign_policy_flyout'; export * from './agent_enrollment_flyout'; export * from './agent_health'; -export * from './agent_unenroll_provider'; +export * from './agent_unenroll_modal'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts index 2c9e8b84d4069..ed6ba5c891a0b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts @@ -25,4 +25,5 @@ export { isPackageLimited, doesAgentPolicyAlreadyIncludePackage, isValidNamespace, + LicenseService, } from '../../../../common'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 30a6742af6ea6..71a44089b8bf7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -49,13 +49,18 @@ export { GetAgentsResponse, GetAgentsRequest, GetOneAgentResponse, + PostAgentUnenrollRequest, PostAgentUnenrollResponse, + PostBulkAgentUnenrollRequest, + PostBulkAgentUnenrollResponse, GetOneAgentEventsRequest, GetOneAgentEventsResponse, GetAgentStatusRequest, GetAgentStatusResponse, PutAgentReassignRequest, PutAgentReassignResponse, + PostBulkAgentReassignRequest, + PostBulkAgentReassignResponse, // API schemas - Enrollment API Keys GetEnrollmentAPIKeysResponse, GetEnrollmentAPIKeysRequest, diff --git a/x-pack/plugins/ingest_manager/public/plugin.ts b/x-pack/plugins/ingest_manager/public/plugin.ts index 536832cdaed64..5f7bfe865e892 100644 --- a/x-pack/plugins/ingest_manager/public/plugin.ts +++ b/x-pack/plugins/ingest_manager/public/plugin.ts @@ -23,7 +23,7 @@ import { BASE_PATH } from './applications/ingest_manager/constants'; import { IngestManagerConfigType } from '../common/types'; import { setupRouteService, appRoutesService } from '../common'; -import { setHttpClient } from './applications/ingest_manager/hooks'; +import { setHttpClient, licenseService } from './applications/ingest_manager/hooks'; import { TutorialDirectoryNotice, TutorialDirectoryHeaderLink, @@ -71,6 +71,9 @@ export class IngestManagerPlugin // Set up http client setHttpClient(core.http); + // Set up license service + licenseService.start(deps.licensing.license$); + // Register main Ingest Manager app core.application.register({ id: PLUGIN_ID, diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index 2ebb7a0667aab..fb867af513fdc 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -16,6 +16,7 @@ import { GetAgentStatusResponse, PutAgentReassignResponse, PostAgentEnrollRequest, + PostBulkAgentReassignResponse, } from '../../../common/types'; import { GetAgentsRequestSchema, @@ -26,11 +27,13 @@ import { PostAgentCheckinRequest, GetAgentStatusRequestSchema, PutAgentReassignRequestSchema, + PostBulkAgentReassignRequestSchema, } from '../../types'; +import { defaultIngestErrorHandler } from '../../errors'; +import { licenseService } from '../../services'; import * as AgentService from '../../services/agents'; import * as APIKeyService from '../../services/api_keys'; import { appContextService } from '../../services/app_context'; -import { defaultIngestErrorHandler } from '../../errors'; export const getAgentHandler: RequestHandler ({ @@ -245,6 +253,7 @@ export const getAgentsHandler: RequestHandler< status: AgentService.getAgentStatus(agent), })), total, + totalInactive, page, perPage, }; @@ -270,6 +279,47 @@ export const putAgentsReassignHandler: RequestHandler< } }; +export const postBulkAgentsReassignHandler: RequestHandler< + undefined, + undefined, + TypeOf +> = async (context, request, response) => { + if (!licenseService.isGoldPlus()) { + return response.customError({ + statusCode: 403, + body: { message: 'Requires Gold license' }, + }); + } + + const soClient = context.core.savedObjects.client; + try { + // Reassign by array of IDs + const result = Array.isArray(request.body.agents) + ? await AgentService.reassignAgents( + soClient, + { agentIds: request.body.agents }, + request.body.policy_id + ) + : await AgentService.reassignAgents( + soClient, + { kuery: request.body.agents }, + request.body.policy_id + ); + const body: PostBulkAgentReassignResponse = result.saved_objects.reduce((acc, so) => { + return { + ...acc, + [so.id]: { + success: !so.error, + error: so.error || undefined, + }, + }; + }, {}); + return response.ok({ body }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } +}; + export const getAgentStatusForAgentPolicyHandler: RequestHandler< undefined, TypeOf diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts index a2e5c742ad6b5..eafc726ea166d 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts @@ -23,9 +23,11 @@ import { PostAgentAcksRequestParamsJSONSchema, PostAgentAcksRequestBodyJSONSchema, PostAgentUnenrollRequestSchema, + PostBulkAgentUnenrollRequestSchema, GetAgentStatusRequestSchema, PostNewAgentActionRequestSchema, PutAgentReassignRequestSchema, + PostBulkAgentReassignRequestSchema, PostAgentEnrollRequestBodyJSONSchema, } from '../../types'; import { @@ -38,12 +40,13 @@ import { postAgentEnrollHandler, getAgentStatusForAgentPolicyHandler, putAgentsReassignHandler, + postBulkAgentsReassignHandler, } from './handlers'; import { postAgentAcksHandlerBuilder } from './acks_handlers'; import * as AgentService from '../../services/agents'; import { postNewAgentActionHandlerBuilder } from './actions_handlers'; import { appContextService } from '../../services'; -import { postAgentsUnenrollHandler } from './unenroll_handler'; +import { postAgentUnenrollHandler, postBulkAgentsUnenrollHandler } from './unenroll_handler'; import { IngestManagerConfigType } from '../..'; const ajv = new Ajv({ @@ -181,7 +184,7 @@ export const registerRoutes = (router: IRouter, config: IngestManagerConfigType) validate: PostAgentUnenrollRequestSchema, options: { tags: [`access:${PLUGIN_ID}-all`] }, }, - postAgentsUnenrollHandler + postAgentUnenrollHandler ); router.put( @@ -212,4 +215,24 @@ export const registerRoutes = (router: IRouter, config: IngestManagerConfigType) }, getAgentStatusForAgentPolicyHandler ); + + // Bulk reassign + router.post( + { + path: AGENT_API_ROUTES.BULK_REASSIGN_PATTERN, + validate: PostBulkAgentReassignRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + postBulkAgentsReassignHandler + ); + + // Bulk unenroll + router.post( + { + path: AGENT_API_ROUTES.BULK_UNENROLL_PATTERN, + validate: PostBulkAgentUnenrollRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + postBulkAgentsUnenrollHandler + ); }; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts b/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts index fa200e912d625..861d7c45c6f0a 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts @@ -6,12 +6,13 @@ import { RequestHandler } from 'src/core/server'; import { TypeOf } from '@kbn/config-schema'; -import { PostAgentUnenrollResponse } from '../../../common/types'; -import { PostAgentUnenrollRequestSchema } from '../../types'; +import { PostAgentUnenrollResponse, PostBulkAgentUnenrollResponse } from '../../../common/types'; +import { PostAgentUnenrollRequestSchema, PostBulkAgentUnenrollRequestSchema } from '../../types'; +import { licenseService } from '../../services'; import * as AgentService from '../../services/agents'; import { defaultIngestErrorHandler } from '../../errors'; -export const postAgentsUnenrollHandler: RequestHandler< +export const postAgentUnenrollHandler: RequestHandler< TypeOf, undefined, TypeOf @@ -30,3 +31,32 @@ export const postAgentsUnenrollHandler: RequestHandler< return defaultIngestErrorHandler({ error, response }); } }; + +export const postBulkAgentsUnenrollHandler: RequestHandler< + undefined, + undefined, + TypeOf +> = async (context, request, response) => { + if (!licenseService.isGoldPlus()) { + return response.customError({ + statusCode: 403, + body: { message: 'Requires Gold license' }, + }); + } + const soClient = context.core.savedObjects.client; + const unenrollAgents = + request.body?.force === true ? AgentService.forceUnenrollAgents : AgentService.unenrollAgents; + + try { + if (Array.isArray(request.body.agents)) { + await unenrollAgents(soClient, { agentIds: request.body.agents }); + } else { + await unenrollAgents(soClient, { kuery: request.body.agents }); + } + + const body: PostBulkAgentUnenrollResponse = {}; + return response.ok({ body }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } +}; diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts index 938cfb4351630..64b11512fae10 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts @@ -26,6 +26,7 @@ import { packagePolicyService } from './package_policy'; import { outputService } from './output'; import { agentPolicyUpdateEventHandler } from './agent_policy_update'; import { getSettings } from './settings'; +import { normalizeKuery } from './saved_object'; const SAVED_OBJECT_TYPE = AGENT_POLICY_SAVED_OBJECT_TYPE; @@ -166,13 +167,7 @@ class AgentPolicyService { sortOrder, page, perPage, - // To ensure users don't need to know about SO data structure... - filter: kuery - ? kuery.replace( - new RegExp(`${SAVED_OBJECT_TYPE}\.`, 'g'), - `${SAVED_OBJECT_TYPE}.attributes.` - ) - : undefined, + filter: kuery ? normalizeKuery(SAVED_OBJECT_TYPE, kuery) : undefined, }); const agentPolicies = await Promise.all( diff --git a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts index 254c2c8b21e32..1d4db44edf88a 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts @@ -29,12 +29,20 @@ export async function createAgentAction( return createAction(soClient, newAgentAction); } +export async function bulkCreateAgentActions( + soClient: SavedObjectsClientContract, + newAgentActions: Array> +): Promise { + return bulkCreateActions(soClient, newAgentActions); +} + export function createAgentPolicyAction( soClient: SavedObjectsClientContract, newAgentAction: Omit ): Promise { return createAction(soClient, newAgentAction); } + async function createAction( soClient: SavedObjectsClientContract, newAgentAction: Omit @@ -47,19 +55,25 @@ async function createAction( soClient: SavedObjectsClientContract, newAgentAction: Omit | Omit ): Promise { - const so = await soClient.create(AGENT_ACTION_SAVED_OBJECT_TYPE, { - ...newAgentAction, - data: newAgentAction.data ? JSON.stringify(newAgentAction.data) : undefined, - ack_data: newAgentAction.ack_data ? JSON.stringify(newAgentAction.ack_data) : undefined, - }); + const actionSO = await soClient.create( + AGENT_ACTION_SAVED_OBJECT_TYPE, + { + ...newAgentAction, + data: newAgentAction.data ? JSON.stringify(newAgentAction.data) : undefined, + ack_data: newAgentAction.ack_data ? JSON.stringify(newAgentAction.ack_data) : undefined, + } + ); - if (isAgentActionSavedObject(so)) { - const agentAction = savedObjectToAgentAction(so); + if (isAgentActionSavedObject(actionSO)) { + const agentAction = savedObjectToAgentAction(actionSO); + // Action `data` is encrypted, so is not returned from the saved object + // so we add back the original value from the request to form the expected + // response shape for POST create agent action endpoint agentAction.data = newAgentAction.data; return agentAction; - } else if (isPolicyActionSavedObject(so)) { - const agentAction = savedObjectToAgentAction(so); + } else if (isPolicyActionSavedObject(actionSO)) { + const agentAction = savedObjectToAgentAction(actionSO); agentAction.data = newAgentAction.data; return agentAction; @@ -67,6 +81,44 @@ async function createAction( throw new Error('Invalid action'); } +async function bulkCreateActions( + soClient: SavedObjectsClientContract, + newAgentActions: Array> +): Promise; +async function bulkCreateActions( + soClient: SavedObjectsClientContract, + newAgentActions: Array> +): Promise; +async function bulkCreateActions( + soClient: SavedObjectsClientContract, + newAgentActions: Array | Omit> +): Promise> { + const { saved_objects: actionSOs } = await soClient.bulkCreate( + newAgentActions.map((newAgentAction) => ({ + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + attributes: { + ...newAgentAction, + data: newAgentAction.data ? JSON.stringify(newAgentAction.data) : undefined, + ack_data: newAgentAction.ack_data ? JSON.stringify(newAgentAction.ack_data) : undefined, + }, + })) + ); + + return actionSOs.map((actionSO) => { + if (isAgentActionSavedObject(actionSO)) { + const agentAction = savedObjectToAgentAction(actionSO); + // Compared to single create (createAction()), we don't add back the + // original value of `agentAction.data` as this method isn't exposed + // via an HTTP endpoint + return agentAction; + } else if (isPolicyActionSavedObject(actionSO)) { + const agentAction = savedObjectToAgentAction(actionSO); + return agentAction; + } + throw new Error('Invalid action'); + }); +} + export async function getAgentActionsForCheckin( soClient: SavedObjectsClientContract, agentId: string diff --git a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts index a57735e25ff7b..c941b0512e597 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts @@ -3,25 +3,37 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import Boom from 'boom'; import { SavedObjectsClientContract } from 'src/core/server'; -import { - AGENT_SAVED_OBJECT_TYPE, - AGENT_EVENT_SAVED_OBJECT_TYPE, - AGENT_TYPE_EPHEMERAL, - AGENT_POLLING_THRESHOLD_MS, -} from '../../constants'; +import { AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentSOAttributes, Agent, AgentEventSOAttributes, ListWithKuery } from '../../types'; +import { escapeSearchQueryPhrase, normalizeKuery, findAllSOs } from '../saved_object'; import { savedObjectToAgent } from './saved_objects'; -import { escapeSearchQueryPhrase } from '../saved_object'; + +const ACTIVE_AGENT_CONDITION = `${AGENT_SAVED_OBJECT_TYPE}.attributes.active:true`; +const INACTIVE_AGENT_CONDITION = `NOT (${ACTIVE_AGENT_CONDITION})`; + +function _joinFilters(filters: string[], operator = 'AND') { + return filters.reduce((acc: string | undefined, filter) => { + if (acc) { + return `${acc} ${operator} (${filter})`; + } + + return `(${filter})`; + }, undefined); +} export async function listAgents( soClient: SavedObjectsClientContract, options: ListWithKuery & { showInactive: boolean; } -) { +): Promise<{ + agents: Agent[]; + total: number; + page: number; + perPage: number; +}> { const { page = 1, perPage = 20, @@ -30,47 +42,86 @@ export async function listAgents( kuery, showInactive = false, } = options; - const filters = []; if (kuery && kuery !== '') { - // To ensure users dont need to know about SO data structure... - filters.push( - kuery.replace( - new RegExp(`${AGENT_SAVED_OBJECT_TYPE}\.`, 'g'), - `${AGENT_SAVED_OBJECT_TYPE}.attributes.` - ) - ); + filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); } if (showInactive === false) { - const agentActiveCondition = `${AGENT_SAVED_OBJECT_TYPE}.attributes.active:true AND not ${AGENT_SAVED_OBJECT_TYPE}.attributes.type:${AGENT_TYPE_EPHEMERAL}`; - const recentlySeenEphemeralAgent = `${AGENT_SAVED_OBJECT_TYPE}.attributes.active:true AND ${AGENT_SAVED_OBJECT_TYPE}.attributes.type:${AGENT_TYPE_EPHEMERAL} AND ${AGENT_SAVED_OBJECT_TYPE}.attributes.last_checkin > ${ - Date.now() - 3 * AGENT_POLLING_THRESHOLD_MS - }`; - filters.push(`(${agentActiveCondition}) OR (${recentlySeenEphemeralAgent})`); + filters.push(ACTIVE_AGENT_CONDITION); } - // eslint-disable-next-line @typescript-eslint/naming-convention - const { saved_objects, total } = await soClient.find({ + const { saved_objects: agentSOs, total } = await soClient.find({ type: AGENT_SAVED_OBJECT_TYPE, + filter: _joinFilters(filters), sortField, sortOrder, page, perPage, - filter: _joinFilters(filters), }); - const agents: Agent[] = saved_objects.map(savedObjectToAgent); - return { - agents, + agents: agentSOs.map(savedObjectToAgent), total, page, perPage, }; } +export async function listAllAgents( + soClient: SavedObjectsClientContract, + options: Omit & { + showInactive: boolean; + } +): Promise<{ + agents: Agent[]; + total: number; +}> { + const { sortField = 'enrolled_at', sortOrder = 'desc', kuery, showInactive = false } = options; + const filters = []; + + if (kuery && kuery !== '') { + filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); + } + + if (showInactive === false) { + filters.push(ACTIVE_AGENT_CONDITION); + } + + const { saved_objects: agentSOs, total } = await findAllSOs(soClient, { + type: AGENT_SAVED_OBJECT_TYPE, + kuery: _joinFilters(filters), + sortField, + sortOrder, + }); + + return { + agents: agentSOs.map(savedObjectToAgent), + total, + }; +} + +export async function countInactiveAgents( + soClient: SavedObjectsClientContract, + options: Pick +): Promise { + const { kuery } = options; + const filters = [INACTIVE_AGENT_CONDITION]; + + if (kuery && kuery !== '') { + filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); + } + + const { total } = await soClient.find({ + type: AGENT_SAVED_OBJECT_TYPE, + filter: _joinFilters(filters), + perPage: 0, + }); + + return total; +} + export async function getAgent(soClient: SavedObjectsClientContract, agentId: string) { const agent = savedObjectToAgent( await soClient.get(AGENT_SAVED_OBJECT_TYPE, agentId) @@ -78,6 +129,17 @@ export async function getAgent(soClient: SavedObjectsClientContract, agentId: st return agent; } +export async function getAgents(soClient: SavedObjectsClientContract, agentIds: string[]) { + const agentSOs = await soClient.bulkGet( + agentIds.map((agentId) => ({ + id: agentId, + type: AGENT_SAVED_OBJECT_TYPE, + })) + ); + const agents = agentSOs.saved_objects.map(savedObjectToAgent); + return agents; +} + export async function getAgentByAccessAPIKeyId( soClient: SavedObjectsClientContract, accessAPIKeyId: string @@ -142,13 +204,3 @@ export async function deleteAgent(soClient: SavedObjectsClientContract, agentId: active: false, }); } - -function _joinFilters(filters: string[], operator = 'AND') { - return filters.reduce((acc: string | undefined, filter) => { - if (acc) { - return `${acc} ${operator} (${filter})`; - } - - return `(${filter})`; - }, undefined); -} diff --git a/x-pack/plugins/ingest_manager/server/services/agents/events.ts b/x-pack/plugins/ingest_manager/server/services/agents/events.ts index dfa599e4ffdfd..627fe4f231d3d 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/events.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/events.ts @@ -7,6 +7,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentEventSOAttributes, AgentEvent } from '../../types'; +import { normalizeKuery } from '../saved_object'; export async function getAgentEvents( soClient: SavedObjectsClientContract, @@ -23,12 +24,7 @@ export async function getAgentEvents( const { total, saved_objects } = await soClient.find({ type: AGENT_EVENT_SAVED_OBJECT_TYPE, filter: - kuery && kuery !== '' - ? kuery.replace( - new RegExp(`${AGENT_EVENT_SAVED_OBJECT_TYPE}\.`, 'g'), - `${AGENT_EVENT_SAVED_OBJECT_TYPE}.attributes.` - ) - : undefined, + kuery && kuery !== '' ? normalizeKuery(AGENT_EVENT_SAVED_OBJECT_TYPE, kuery) : undefined, perPage, page, sortField: 'timestamp', diff --git a/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts b/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts index 3075e146093e3..345c07511f032 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts @@ -9,6 +9,7 @@ import Boom from 'boom'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentSOAttributes } from '../../types'; import { agentPolicyService } from '../agent_policy'; +import { getAgents, listAllAgents } from './crud'; export async function reassignAgent( soClient: SavedObjectsClientContract, @@ -25,3 +26,44 @@ export async function reassignAgent( policy_revision: null, }); } + +export async function reassignAgents( + soClient: SavedObjectsClientContract, + options: + | { + agentIds: string[]; + } + | { + kuery: string; + }, + newAgentPolicyId: string +) { + const agentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); + if (!agentPolicy) { + throw Boom.notFound(`Agent policy not found: ${newAgentPolicyId}`); + } + + // Filter to agents that do not already use the new agent policy ID + const agents = + 'agentIds' in options + ? await getAgents(soClient, options.agentIds) + : ( + await listAllAgents(soClient, { + kuery: options.kuery, + showInactive: false, + }) + ).agents; + const agentsToUpdate = agents.filter((agent) => agent.policy_id !== newAgentPolicyId); + + // Update the necessary agents + return await soClient.bulkUpdate( + agentsToUpdate.map((agent) => ({ + type: AGENT_SAVED_OBJECT_TYPE, + id: agent.id, + attributes: { + policy_id: newAgentPolicyId, + policy_revision: null, + }, + })) + ); +} diff --git a/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts index e0ac2620cafd3..60533e1285141 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts @@ -3,13 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { chunk } from 'lodash'; import { SavedObjectsClientContract } from 'src/core/server'; import { AgentSOAttributes } from '../../types'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { getAgent } from './crud'; import * as APIKeyService from '../api_keys'; -import { createAgentAction } from './actions'; +import { createAgentAction, bulkCreateAgentActions } from './actions'; +import { getAgents, listAllAgents } from './crud'; export async function unenrollAgent(soClient: SavedObjectsClientContract, agentId: string) { const now = new Date().toISOString(); @@ -23,6 +24,53 @@ export async function unenrollAgent(soClient: SavedObjectsClientContract, agentI }); } +export async function unenrollAgents( + soClient: SavedObjectsClientContract, + options: + | { + agentIds: string[]; + } + | { + kuery: string; + } +) { + // Filter to agents that do not already unenrolled, or unenrolling + const agents = + 'agentIds' in options + ? await getAgents(soClient, options.agentIds) + : ( + await listAllAgents(soClient, { + kuery: options.kuery, + showInactive: false, + }) + ).agents; + const agentsToUpdate = agents.filter( + (agent) => !agent.unenrollment_started_at && !agent.unenrolled_at + ); + const now = new Date().toISOString(); + + // Create unenroll action for each agent + await bulkCreateAgentActions( + soClient, + agentsToUpdate.map((agent) => ({ + agent_id: agent.id, + created_at: now, + type: 'UNENROLL', + })) + ); + + // Update the necessary agents + return await soClient.bulkUpdate( + agentsToUpdate.map((agent) => ({ + type: AGENT_SAVED_OBJECT_TYPE, + id: agent.id, + attributes: { + unenrollment_started_at: now, + }, + })) + ); +} + export async function forceUnenrollAgent(soClient: SavedObjectsClientContract, agentId: string) { const agent = await getAgent(soClient, agentId); @@ -40,3 +88,63 @@ export async function forceUnenrollAgent(soClient: SavedObjectsClientContract, a unenrolled_at: new Date().toISOString(), }); } + +export async function forceUnenrollAgents( + soClient: SavedObjectsClientContract, + options: + | { + agentIds: string[]; + } + | { + kuery: string; + } +) { + // Filter to agents that are not already unenrolled + const agents = + 'agentIds' in options + ? await getAgents(soClient, options.agentIds) + : ( + await listAllAgents(soClient, { + kuery: options.kuery, + showInactive: false, + }) + ).agents; + const agentsToUpdate = agents.filter((agent) => !agent.unenrolled_at); + const now = new Date().toISOString(); + const apiKeys: string[] = []; + + // Get all API keys that need to be invalidated + agentsToUpdate.forEach((agent) => { + if (agent.access_api_key_id) { + apiKeys.push(agent.access_api_key_id); + } + if (agent.default_api_key_id) { + apiKeys.push(agent.default_api_key_id); + } + }); + + // Invalidate all API keys + // ES doesn't provide a bulk invalidate API, so this could take a long time depending on + // number of keys to invalidate. We run these in batches to avoid overloading ES. + if (apiKeys.length) { + const BATCH_SIZE = 500; + const batches = chunk(apiKeys, BATCH_SIZE); + for (const apiKeysBatch of batches) { + await Promise.all( + apiKeysBatch.map((apiKey) => APIKeyService.invalidateAPIKey(soClient, apiKey)) + ); + } + } + + // Update the necessary agents + return await soClient.bulkUpdate( + agentsToUpdate.map((agent) => ({ + type: AGENT_SAVED_OBJECT_TYPE, + id: agent.id, + attributes: { + active: false, + unenrolled_at: now, + }, + })) + ); +} diff --git a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts index f058166fc2a4f..ea5d25dc9884f 100644 --- a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts @@ -12,6 +12,7 @@ import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; import { createAPIKey, invalidateAPIKey } from './security'; import { agentPolicyService } from '../agent_policy'; import { appContextService } from '../app_context'; +import { normalizeKuery } from '../saved_object'; export async function listEnrollmentApiKeys( soClient: SavedObjectsClientContract, @@ -33,10 +34,7 @@ export async function listEnrollmentApiKeys( sortOrder: 'desc', filter: kuery && kuery !== '' - ? kuery.replace( - new RegExp(`${ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE}\.`, 'g'), - `${ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE}.attributes.` - ) + ? normalizeKuery(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, kuery) : undefined, }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts index b788d1bcbb4a9..6618220a27085 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts @@ -29,9 +29,8 @@ const getDefaultRegistryUrl = (): string => { }; export const getRegistryUrl = (): string => { - const license = licenseService.getLicenseInformation(); const customUrl = appContextService.getConfig()?.registryUrl; - const isGoldPlus = license?.isAvailable && license?.isActive && license?.hasAtLeast('gold'); + const isGoldPlus = licenseService.isGoldPlus(); if (customUrl && isGoldPlus) { return customUrl; diff --git a/x-pack/plugins/ingest_manager/server/services/index.ts b/x-pack/plugins/ingest_manager/server/services/index.ts index 5942277e90824..7a62c307973c2 100644 --- a/x-pack/plugins/ingest_manager/server/services/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/index.ts @@ -7,6 +7,7 @@ import { SavedObjectsClientContract, KibanaRequest } from 'kibana/server'; import { AgentStatus, Agent, EsAssetReference } from '../types'; import * as settingsService from './settings'; +import { getAgent, listAgents } from './agents'; export { ESIndexPatternSavedObjectService } from './es_index_pattern'; export { getRegistryUrl } from './epm/registry/registry_url'; @@ -40,7 +41,7 @@ export interface AgentService { /** * Get an Agent by id */ - getAgent(soClient: SavedObjectsClientContract, agentId: string): Promise; + getAgent: typeof getAgent; /** * Authenticate an agent with access toekn */ @@ -55,20 +56,7 @@ export interface AgentService { /** * List agents */ - listAgents( - soClient: SavedObjectsClientContract, - options: { - page: number; - perPage: number; - kuery?: string; - showInactive: boolean; - } - ): Promise<{ - agents: Agent[]; - total: number; - page: number; - perPage: number; - }>; + listAgents: typeof listAgents; } // Saved object services diff --git a/x-pack/plugins/ingest_manager/server/services/license.ts b/x-pack/plugins/ingest_manager/server/services/license.ts index bd96dbc7e3aff..a67ec9880ec09 100644 --- a/x-pack/plugins/ingest_manager/server/services/license.ts +++ b/x-pack/plugins/ingest_manager/server/services/license.ts @@ -3,36 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Observable, Subscription } from 'rxjs'; -import { ILicense } from '../../../licensing/server'; - -class LicenseService { - private observable: Observable | null = null; - private subscription: Subscription | null = null; - private licenseInformation: ILicense | null = null; - - private updateInformation(licenseInformation: ILicense) { - this.licenseInformation = licenseInformation; - } - - public start(license$: Observable) { - this.observable = license$; - this.subscription = this.observable.subscribe(this.updateInformation.bind(this)); - } - - public stop() { - if (this.subscription) { - this.subscription.unsubscribe(); - } - } - - public getLicenseInformation() { - return this.licenseInformation; - } - - public getLicenseInformation$() { - return this.observable; - } -} +import { LicenseService } from '../../common'; export const licenseService = new LicenseService(); diff --git a/x-pack/plugins/ingest_manager/server/services/package_policy.ts b/x-pack/plugins/ingest_manager/server/services/package_policy.ts index b7e1806979db8..3a02544250ff0 100644 --- a/x-pack/plugins/ingest_manager/server/services/package_policy.ts +++ b/x-pack/plugins/ingest_manager/server/services/package_policy.ts @@ -30,6 +30,7 @@ import * as Registry from './epm/registry'; import { getPackageInfo, getInstallation, ensureInstalledPackage } from './epm/packages'; import { getAssetsData } from './epm/packages/assets'; import { createStream } from './epm/agent/agent'; +import { normalizeKuery } from './saved_object'; const SAVED_OBJECT_TYPE = PACKAGE_POLICY_SAVED_OBJECT_TYPE; @@ -211,13 +212,7 @@ class PackagePolicyService { sortOrder, page, perPage, - // To ensure users don't need to know about SO data structure... - filter: kuery - ? kuery.replace( - new RegExp(`${SAVED_OBJECT_TYPE}\.`, 'g'), - `${SAVED_OBJECT_TYPE}.attributes.` - ) - : undefined, + filter: kuery ? normalizeKuery(SAVED_OBJECT_TYPE, kuery) : undefined, }); return { diff --git a/x-pack/plugins/ingest_manager/server/services/saved_object.ts b/x-pack/plugins/ingest_manager/server/services/saved_object.ts index 8fe7ffcdfc896..06772206d5198 100644 --- a/x-pack/plugins/ingest_manager/server/services/saved_object.ts +++ b/x-pack/plugins/ingest_manager/server/services/saved_object.ts @@ -3,6 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { SavedObjectsClientContract, SavedObjectsFindResponse } from 'src/core/server'; +import { ListWithKuery } from '../types'; /** * Escape a value with double quote to use with saved object search @@ -12,3 +14,70 @@ export function escapeSearchQueryPhrase(val: string): string { return `"${val.replace(/["]/g, '"')}"`; } + +// Adds `.attribute` to any kuery strings that are missing it, this comes from +// internal SO structure. Kuery strings that come from UI will typicall have +// `.attribute` hidden to simplify UX, so this normalizes any kuery string for +// filtering SOs +export const normalizeKuery = (savedObjectType: string, kuery: string): string => { + return kuery.replace( + new RegExp(`${savedObjectType}\.(?!attributes\.)`, 'g'), + `${savedObjectType}.attributes.` + ); +}; + +// Like saved object client `.find()`, but ignores `page` and `perPage` parameters and +// returns *all* matching saved objects by collocating results from all `.find` pages. +// This function actually doesn't offer any additional benefits over `.find()` for now +// due to SO client limitations (see comments below), so is a placeholder for when SO +// client is improved. +export const findAllSOs = async ( + soClient: SavedObjectsClientContract, + options: Omit & { + type: string; + } +): Promise, 'saved_objects' | 'total'>> => { + const { type, sortField, sortOrder, kuery } = options; + let savedObjectResults: SavedObjectsFindResponse['saved_objects'] = []; + + // TODO: This is the default `index.max_result_window` ES setting, which dictates + // the maximum amount of results allowed to be returned from a search. It's possible + // for the actual setting to differ from the default. Can we retrieve the real + // setting in the future? + const searchLimit = 10000; + + const query = { + type, + sortField, + sortOrder, + filter: kuery, + page: 1, + perPage: searchLimit, + }; + + const { saved_objects: initialSOs, total } = await soClient.find(query); + + savedObjectResults = initialSOs; + + // The saved object client can't actually page through more than the first 10,000 + // results, due to the same `index.max_result_window` constraint. The commented out + // code below is an example of paging through rest of results when the SO client + // offers that kind of support. + // if (total > searchLimit) { + // const remainingPages = Math.ceil((total - searchLimit) / searchLimit); + // for (let currentPage = 2; currentPage <= remainingPages + 1; currentPage++) { + // const { saved_objects: currentPageSavedObjects } = await soClient.find({ + // ...query, + // page: currentPage, + // }); + // if (currentPageSavedObjects.length) { + // savedObjectResults = savedObjectResults.concat(currentPageSavedObjects); + // } + // } + // } + + return { + saved_objects: savedObjectResults, + total, + }; +}; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts index 43ee0c89126e9..4aefa56e0ca0a 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts @@ -172,6 +172,13 @@ export const PostAgentUnenrollRequestSchema = { ), }; +export const PostBulkAgentUnenrollRequestSchema = { + body: schema.object({ + agents: schema.oneOf([schema.arrayOf(schema.string()), schema.string()]), + force: schema.maybe(schema.boolean()), + }), +}; + export const PutAgentReassignRequestSchema = { params: schema.object({ agentId: schema.string(), @@ -181,6 +188,13 @@ export const PutAgentReassignRequestSchema = { }), }; +export const PostBulkAgentReassignRequestSchema = { + body: schema.object({ + policy_id: schema.string(), + agents: schema.oneOf([schema.arrayOf(schema.string()), schema.string()]), + }), +}; + export const GetOneAgentEventsRequestSchema = { params: schema.object({ agentId: schema.string(), diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9668bdf4e6781..afcbf89ae80ad 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9288,15 +9288,7 @@ "xpack.ingestManager.setupPage.missingRequirementsKibanaTitle": "Kibana構成で、次の項目を有効にします。", "xpack.ingestManager.setupPage.tlsFlagText": "{kibanaSecurityLink}.{securityFlag}を{true}に設定します。開発目的では、危険な代替として{tlsFlag}を{true}に設定して、{tlsLink}を無効化できます。", "xpack.ingestManager.setupPage.tlsLink": "TLS", - "xpack.ingestManager.unenrollAgents.confirmModal.cancelButtonLabel": "キャンセル", - "xpack.ingestManager.unenrollAgents.confirmModal.confirmButtonLabel": "登録解除", - "xpack.ingestManager.unenrollAgents.confirmModal.deleteMultipleTitle": "{count, plural, one {# エージェント} other {# エージェント}}の登録を解除しますか?", - "xpack.ingestManager.unenrollAgents.confirmModal.deleteSingleTitle": "エージェント「{id}」の登録を解除しますか?", - "xpack.ingestManager.unenrollAgents.confirmModal.forceDeleteSingleTitle": "強制的にエージェント「{id}」の登録を解除しますか?", - "xpack.ingestManager.unenrollAgents.confirmModal.loadingButtonLabel": "読み込み中...", - "xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle": "エージェントの登録解除エラー", - "xpack.ingestManager.unenrollAgents.successForceSingleNotificationTitle": "エージェント「{id}」の登録を解除しました", - "xpack.ingestManager.unenrollAgents.successSingleNotificationTitle": "エージェント「{id}」を登録解除しています", + "xpack.ingestManager.unenrollAgents.cancelButtonLabel": "キャンセル", "xpack.ingestPipelines.app.checkingPrivilegesDescription": "権限を確認中…", "xpack.ingestPipelines.app.checkingPrivilegesErrorMessage": "サーバーからユーザー特権を取得中にエラーが発生。", "xpack.ingestPipelines.app.deniedPrivilegeDescription": "Ingest Pipelinesを使用するには、{privilegesCount, plural, one {このクラスター特権} other {これらのクラスター特権}}が必要です:{missingPrivileges}。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f946709f7c23b..e5dfbe60eb88a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9294,15 +9294,7 @@ "xpack.ingestManager.setupPage.missingRequirementsKibanaTitle": "在您的 Kibana 配置中,启用:", "xpack.ingestManager.setupPage.tlsFlagText": "{kibanaSecurityLink}。将 {securityFlag} 设置为 {true}。出于开发目的,作为非安全的备用方案可以通过将 {tlsFlag} 设置为 {true} 来禁用 {tlsLink}。", "xpack.ingestManager.setupPage.tlsLink": "TLS", - "xpack.ingestManager.unenrollAgents.confirmModal.cancelButtonLabel": "取消", - "xpack.ingestManager.unenrollAgents.confirmModal.confirmButtonLabel": "取消注册", - "xpack.ingestManager.unenrollAgents.confirmModal.deleteMultipleTitle": "取消注册 {count, plural, one {# 个代理} other {# 个代理}}?", - "xpack.ingestManager.unenrollAgents.confirmModal.deleteSingleTitle": "取消注册“{id}”?", - "xpack.ingestManager.unenrollAgents.confirmModal.forceDeleteSingleTitle": "强制取消注册代理“{id}”?", - "xpack.ingestManager.unenrollAgents.confirmModal.loadingButtonLabel": "正在加载……", - "xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle": "取消注册代理时出错", - "xpack.ingestManager.unenrollAgents.successForceSingleNotificationTitle": "代理“{id}”已取消注册", - "xpack.ingestManager.unenrollAgents.successSingleNotificationTitle": "取消注册代理“{id}”", + "xpack.ingestManager.unenrollAgents.cancelButtonLabel": "取消", "xpack.ingestPipelines.app.checkingPrivilegesDescription": "正在检查权限……", "xpack.ingestPipelines.app.checkingPrivilegesErrorMessage": "从服务器获取用户权限时出错。", "xpack.ingestPipelines.app.deniedPrivilegeDescription": "要使用“采集管道”,必须具有{privilegesCount, plural, one {以下集群权限} other {以下集群权限}}:{missingPrivileges}。", diff --git a/x-pack/test/functional/es_archives/fleet/agents/data.json b/x-pack/test/functional/es_archives/fleet/agents/data.json index 12d646de85ec3..e05a2fe010e89 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/data.json +++ b/x-pack/test/functional/es_archives/fleet/agents/data.json @@ -32,6 +32,7 @@ "access_api_key_id": "api-key-2", "active": true, "shared_id": "agent2_filebeat", + "policy_id": "policy1", "type": "PERMANENT", "local_metadata": {}, "user_provided_metadata": {} @@ -54,6 +55,7 @@ "access_api_key_id": "api-key-3", "active": true, "shared_id": "agent3_metricbeat", + "policy_id": "policy1", "type": "PERMANENT", "local_metadata": {}, "user_provided_metadata": {} @@ -76,6 +78,7 @@ "access_api_key_id": "api-key-4", "active": true, "shared_id": "agent4_metricbeat", + "policy_id": "policy1", "type": "PERMANENT", "local_metadata": {}, "user_provided_metadata": {} @@ -246,3 +249,32 @@ } } } + +{ + "type": "doc", + "value": { + "id": "ingest-agent-policies:policy2", + "index": ".kibana", + "source": { + "type": "ingest-agent-policies", + "ingest-agent-policies": { + "name": "Test policy 2", + "namespace": "default", + "description": "Policy 2", + "status": "active", + "package_policies": [], + "is_default": true, + "monitoring_enabled": [ + "logs", + "metrics" + ], + "revision": 2, + "updated_at": "2020-05-07T19:34:42.533Z", + "updated_by": "system" + }, + "migrationVersion": { + "ingest-agent-policies": "7.10.0" + } + } + } +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/reassign.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/reassign.ts new file mode 100644 index 0000000000000..f3e24fab1dc2a --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/reassign.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../api_integration/ftr_provider_context'; +import { setupIngest } from './services'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + + describe('fleet_reassign_agent', () => { + setupIngest(providerContext); + before(async () => { + await esArchiver.loadIfNeeded('fleet/agents'); + }); + after(async () => { + await esArchiver.unload('fleet/agents'); + }); + + it('should allow to reassign single agent', async () => { + await supertest + .put(`/api/ingest_manager/fleet/agents/agent1/reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy2', + }) + .expect(200); + const { body } = await supertest + .get(`/api/ingest_manager/fleet/agents/agent1`) + .set('kbn-xsrf', 'xxx'); + expect(body.item.policy_id).to.eql('policy2'); + }); + + it('should throw an error for invalid policy id for single reassign', async () => { + await supertest + .put(`/api/ingest_manager/fleet/agents/agent1/reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'INVALID_ID', + }) + .expect(404); + }); + + it('should allow to reassign multiple agents by id', async () => { + await supertest + .post(`/api/ingest_manager/fleet/agents/bulk_reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent2', 'agent3'], + policy_id: 'policy2', + }) + .expect(200); + const [agent2data, agent3data] = await Promise.all([ + supertest.get(`/api/ingest_manager/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/ingest_manager/fleet/agents/agent3`).set('kbn-xsrf', 'xxx'), + ]); + expect(agent2data.body.item.policy_id).to.eql('policy2'); + expect(agent3data.body.item.policy_id).to.eql('policy2'); + }); + + it('should allow to reassign multiple agents by kuery', async () => { + await supertest + .post(`/api/ingest_manager/fleet/agents/bulk_reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: 'fleet-agents.active: true', + policy_id: 'policy2', + }) + .expect(200); + const { body } = await supertest + .get(`/api/ingest_manager/fleet/agents`) + .set('kbn-xsrf', 'xxx'); + expect(body.total).to.eql(4); + body.list.forEach((agent: any) => { + expect(agent.policy_id).to.eql('policy2'); + }); + }); + + it('should throw an error for invalid policy id for bulk reassign', async () => { + await supertest + .post(`/api/ingest_manager/fleet/agents/bulk_reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent2', 'agent3'], + policy_id: 'INVALID_ID', + }) + .expect(404); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/unenroll.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/unenroll.ts index d24e438fa13ea..ce5dfd7714ab2 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/unenroll.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/unenroll.ts @@ -64,7 +64,7 @@ export default function (providerContext: FtrProviderContext) { await esArchiver.unload('fleet/agents'); }); - it('allow to unenroll using a list of ids', async () => { + it('should allow to unenroll single agent', async () => { await supertest .post(`/api/ingest_manager/fleet/agents/agent1/unenroll`) .set('kbn-xsrf', 'xxx') @@ -95,5 +95,39 @@ export default function (providerContext: FtrProviderContext) { expect(outputAPIKeys).length(1); expect(outputAPIKeys[0].invalidated).eql(true); }); + + it('should allow to unenroll multiple agents by id', async () => { + await supertest + .post(`/api/ingest_manager/fleet/agents/bulk_unenroll`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent2', 'agent3'], + }) + .expect(200); + const [agent2data, agent3data] = await Promise.all([ + supertest.get(`/api/ingest_manager/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/ingest_manager/fleet/agents/agent3`).set('kbn-xsrf', 'xxx'), + ]); + expect(typeof agent2data.body.item.unenrollment_started_at).to.eql('string'); + expect(agent2data.body.item.active).to.eql(true); + expect(typeof agent3data.body.item.unenrollment_started_at).to.be('string'); + expect(agent2data.body.item.active).to.eql(true); + }); + + it('should allow to unenroll multiple agents by kuery', async () => { + await supertest + .post(`/api/ingest_manager/fleet/agents/bulk_unenroll`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: 'fleet-agents.active: true', + force: true, + }) + .expect(200); + + const { body } = await supertest + .get(`/api/ingest_manager/fleet/agents`) + .set('kbn-xsrf', 'xxx'); + expect(body.total).to.eql(0); + }); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/enrollment_api_keys/crud.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/enrollment_api_keys/crud.ts index 6c5d552a51eb9..b3519e0ccc2a3 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/enrollment_api_keys/crud.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/enrollment_api_keys/crud.ts @@ -36,7 +36,7 @@ export default function (providerContext: FtrProviderContext) { .get(`/api/ingest_manager/fleet/enrollment-api-keys`) .expect(200); - expect(apiResponse.total).to.be(2); + expect(apiResponse.total).to.be(3); expect(apiResponse.list[0]).to.have.keys('id', 'api_key_id', 'name'); }); }); diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/index.js b/x-pack/test/ingest_manager_api_integration/apis/fleet/index.js index 3a72fe6d9f12b..96b9ffd1b04c0 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/index.js @@ -18,5 +18,6 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./enrollment_api_keys/crud')); loadTestFile(require.resolve('./install')); loadTestFile(require.resolve('./agents/actions')); + loadTestFile(require.resolve('./agents/reassign')); }); } From 07b7b06c116f22dd9f490cf095a22814bae79d3c Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 22 Sep 2020 14:47:11 -0700 Subject: [PATCH 24/92] Remove requirement for manage_index_templates privilege for Index Management (#77377) --- x-pack/plugins/index_management/server/plugin.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts index 30aeeb6b45362..ae9633f3e22b9 100644 --- a/x-pack/plugins/index_management/server/plugin.ts +++ b/x-pack/plugins/index_management/server/plugin.ts @@ -84,7 +84,9 @@ export class IndexMgmtServerPlugin implements Plugin Date: Tue, 22 Sep 2020 18:03:22 -0500 Subject: [PATCH 25/92] Remove [key: string]: any; from IIndexPattern (#77968) * Remove [key: string]: any; from IIndexPattern --- ...blic.iindexpattern.getformatterforfield.md | 11 + ...lugin-plugins-data-public.iindexpattern.md | 1 + ...ublic.indexpattern.getformatterforfield.md | 4 +- ...ns-data-public.indexpatternfield.tospec.md | 38 +--- .../kibana-plugin-plugins-data-public.md | 6 +- ...ata-server.fielddescriptor.aggregatable.md | 11 + ...ins-data-server.fielddescriptor.estypes.md | 11 + ...gin-plugins-data-server.fielddescriptor.md | 24 ++ ...lugins-data-server.fielddescriptor.name.md | 11 + ...erver.fielddescriptor.readfromdocvalues.md | 11 + ...-data-server.fielddescriptor.searchable.md | 11 + ...ins-data-server.fielddescriptor.subtype.md | 11 + ...lugins-data-server.fielddescriptor.type.md | 11 + ...ata-server.iindexpattern.fieldformatmap.md | 14 -- ...lugins-data-server.iindexpattern.fields.md | 11 - ...-data-server.iindexpattern.gettimefield.md | 15 -- ...in-plugins-data-server.iindexpattern.id.md | 11 - ...lugin-plugins-data-server.iindexpattern.md | 29 --- ...data-server.iindexpattern.timefieldname.md | 11 - ...plugins-data-server.iindexpattern.title.md | 11 - ...-plugins-data-server.iindexpattern.type.md | 11 - ...-data-server.indexpattern._constructor_.md | 21 ++ ...s-data-server.indexpattern._fetchfields.md | 15 ++ ...ta-server.indexpattern.addscriptedfield.md | 25 +++ ...plugins-data-server.indexpattern.create.md | 22 ++ ...data-server.indexpattern.fieldformatmap.md | 11 + ...plugins-data-server.indexpattern.fields.md | 13 ++ ...-data-server.indexpattern.fieldsfetcher.md | 11 + ...ins-data-server.indexpattern.flattenhit.md | 11 + ...ns-data-server.indexpattern.formatfield.md | 11 + ...gins-data-server.indexpattern.formathit.md | 11 + ...indexpattern.getaggregationrestrictions.md | 29 +++ ...a-server.indexpattern.getcomputedfields.md | 29 +++ ...data-server.indexpattern.getfieldbyname.md | 22 ++ ...erver.indexpattern.getformatterforfield.md | 22 ++ ...erver.indexpattern.getnonscriptedfields.md | 15 ++ ...a-server.indexpattern.getscriptedfields.md | 15 ++ ...-server.indexpattern.getsourcefiltering.md | 19 ++ ...s-data-server.indexpattern.gettimefield.md | 15 ++ ...gin-plugins-data-server.indexpattern.id.md | 11 + ...n-plugins-data-server.indexpattern.init.md | 15 ++ ...s-data-server.indexpattern.initfromspec.md | 22 ++ ...s-data-server.indexpattern.intervalname.md | 11 + ...ns-data-server.indexpattern.istimebased.md | 15 ++ ...server.indexpattern.istimebasedwildcard.md | 15 ++ ...ta-server.indexpattern.istimenanosbased.md | 15 ++ ...ins-data-server.indexpattern.iswildcard.md | 15 ++ ...plugin-plugins-data-server.indexpattern.md | 66 ++++++ ...ins-data-server.indexpattern.metafields.md | 11 + ...s-data-server.indexpattern.originalbody.md | 13 ++ ...ata-server.indexpattern.popularizefield.md | 23 ++ ...ugins-data-server.indexpattern.prepbody.md | 33 +++ ...-data-server.indexpattern.refreshfields.md | 15 ++ ...server.indexpattern.removescriptedfield.md | 22 ++ ...-data-server.indexpattern.sourcefilters.md | 11 + ...-data-server.indexpattern.timefieldname.md | 11 + ...-plugins-data-server.indexpattern.title.md | 11 + ...plugins-data-server.indexpattern.tospec.md | 15 ++ ...n-plugins-data-server.indexpattern.type.md | 11 + ...ugins-data-server.indexpattern.typemeta.md | 11 + ...lugins-data-server.indexpattern.version.md | 11 + ...ndexpatternfielddescriptor.aggregatable.md | 11 - ...ver.indexpatternfielddescriptor.estypes.md | 11 - ...data-server.indexpatternfielddescriptor.md | 24 -- ...server.indexpatternfielddescriptor.name.md | 11 - ...atternfielddescriptor.readfromdocvalues.md | 11 - ....indexpatternfielddescriptor.searchable.md | 11 - ...ver.indexpatternfielddescriptor.subtype.md | 11 - ...server.indexpatternfielddescriptor.type.md | 11 - .../kibana-plugin-plugins-data-server.md | 4 +- .../fields/index_pattern_field.ts | 2 +- .../index_patterns/index_pattern.ts | 5 +- .../data/common/index_patterns/types.ts | 6 +- src/plugins/data/public/public.api.md | 25 +-- src/plugins/data/server/index.ts | 3 +- .../data/server/index_patterns/utils.ts | 10 +- src/plugins/data/server/server.api.md | 212 +++++++++++++----- .../components/discover_legacy.tsx | 4 +- .../indexed_fields_table.test.tsx | 4 +- .../indexed_fields_table.tsx | 10 +- .../components/table/table.test.tsx | 6 +- .../components/table/table.tsx | 4 +- .../get_dynamic_index_pattern.ts | 11 +- .../server/maps_telemetry/maps_telemetry.ts | 32 ++- .../data_frame_analytics/index_patterns.ts | 4 +- .../csv_from_savedobject/lib/get_csv_job.ts | 11 +- .../rules/description_step/helpers.test.tsx | 5 +- .../uptime/server/lib/alerts/status_check.ts | 6 +- .../server/lib/requests/get_index_pattern.ts | 17 +- 89 files changed, 1063 insertions(+), 393 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.aggregatable.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.estypes.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.name.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.readfromdocvalues.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.searchable.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.subtype.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.type.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.fieldformatmap.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.fields.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.gettimefield.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.id.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.timefieldname.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.title.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.type.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._constructor_.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._fetchfields.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.create.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fields.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldsfetcher.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.flattenhit.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formatfield.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formathit.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getaggregationrestrictions.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldbyname.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.gettimefield.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.id.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.init.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.initfromspec.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.intervalname.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimebased.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimebasedwildcard.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimenanosbased.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.iswildcard.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.metafields.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.originalbody.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.popularizefield.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.prepbody.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.refreshfields.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.sourcefilters.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.timefieldname.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.title.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.tospec.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.type.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.typemeta.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.version.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.aggregatable.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.estypes.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.name.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.readfromdocvalues.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.searchable.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.subtype.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.type.md diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md new file mode 100644 index 0000000000000..7466e4b9cf658 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) > [getFormatterForField](./kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md) + +## IIndexPattern.getFormatterForField property + +Signature: + +```typescript +getFormatterForField?: (field: IndexPatternField | IndexPatternField['spec'] | IFieldType) => FieldFormat; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md index 1cb89822eb605..b631d4dd7800a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md @@ -16,6 +16,7 @@ export interface IIndexPattern | --- | --- | --- | | [fieldFormatMap](./kibana-plugin-plugins-data-public.iindexpattern.fieldformatmap.md) | Record<string, {
id: string;
params: unknown;
}> | | | [fields](./kibana-plugin-plugins-data-public.iindexpattern.fields.md) | IFieldType[] | | +| [getFormatterForField](./kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md) | (field: IndexPatternField | IndexPatternField['spec'] | IFieldType) => FieldFormat | | | [id](./kibana-plugin-plugins-data-public.iindexpattern.id.md) | string | | | [timeFieldName](./kibana-plugin-plugins-data-public.iindexpattern.timefieldname.md) | string | | | [title](./kibana-plugin-plugins-data-public.iindexpattern.title.md) | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md index 7984f7aff1d2d..180b2d8a7b03a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md @@ -7,14 +7,14 @@ Signature: ```typescript -getFormatterForField(field: IndexPatternField | IndexPatternField['spec']): FieldFormat; +getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| field | IndexPatternField | IndexPatternField['spec'] | | +| field | IndexPatternField | IndexPatternField['spec'] | IFieldType | | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md index 1d80c90991f55..711d6ad660450 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md @@ -9,24 +9,7 @@ ```typescript toSpec({ getFormatterForField, }?: { getFormatterForField?: IndexPattern['getFormatterForField']; - }): { - count: number; - script: string | undefined; - lang: string | undefined; - conflictDescriptions: Record | undefined; - name: string; - type: string; - esTypes: string[] | undefined; - scripted: boolean; - searchable: boolean; - aggregatable: boolean; - readFromDocValues: boolean; - subType: import("../types").IFieldSubType | undefined; - format: { - id: any; - params: any; - } | undefined; - }; + }): FieldSpec; ``` ## Parameters @@ -37,22 +20,5 @@ toSpec({ getFormatterForField, }?: { Returns: -`{ - count: number; - script: string | undefined; - lang: string | undefined; - conflictDescriptions: Record | undefined; - name: string; - type: string; - esTypes: string[] | undefined; - scripted: boolean; - searchable: boolean; - aggregatable: boolean; - readFromDocValues: boolean; - subType: import("../types").IFieldSubType | undefined; - format: { - id: any; - params: any; - } | undefined; - }` +`FieldSpec` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index f51549c81fb62..71f66a1b46d85 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -57,7 +57,6 @@ | [EsQueryConfig](./kibana-plugin-plugins-data-public.esqueryconfig.md) | | | [FieldFormatConfig](./kibana-plugin-plugins-data-public.fieldformatconfig.md) | | | [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) | | -| [Filter](./kibana-plugin-plugins-data-public.filter.md) | | | [IDataPluginServices](./kibana-plugin-plugins-data-public.idatapluginservices.md) | | | [IEsSearchRequest](./kibana-plugin-plugins-data-public.iessearchrequest.md) | | | [IEsSearchResponse](./kibana-plugin-plugins-data-public.iessearchresponse.md) | | @@ -75,7 +74,6 @@ | [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | high level search service | | [KueryNode](./kibana-plugin-plugins-data-public.kuerynode.md) | | | [OptionedValueProp](./kibana-plugin-plugins-data-public.optionedvalueprop.md) | | -| [Query](./kibana-plugin-plugins-data-public.query.md) | | | [QueryState](./kibana-plugin-plugins-data-public.querystate.md) | All query state service state | | [QueryStateChange](./kibana-plugin-plugins-data-public.querystatechange.md) | | | [QuerySuggestionBasic](./kibana-plugin-plugins-data-public.querysuggestionbasic.md) | \* | @@ -90,7 +88,6 @@ | [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields | | [TabbedAggColumn](./kibana-plugin-plugins-data-public.tabbedaggcolumn.md) | \* | | [TabbedTable](./kibana-plugin-plugins-data-public.tabbedtable.md) | \* | -| [TimeRange](./kibana-plugin-plugins-data-public.timerange.md) | | ## Variables @@ -145,6 +142,7 @@ | [FieldFormatsContentType](./kibana-plugin-plugins-data-public.fieldformatscontenttype.md) | \* | | [FieldFormatsGetConfigFn](./kibana-plugin-plugins-data-public.fieldformatsgetconfigfn.md) | | | [FieldFormatsStart](./kibana-plugin-plugins-data-public.fieldformatsstart.md) | | +| [Filter](./kibana-plugin-plugins-data-public.filter.md) | | | [IAggConfig](./kibana-plugin-plugins-data-public.iaggconfig.md) | AggConfig This class represents an aggregation, which is displayed in the left-hand nav of the Visualize app. | | [IAggType](./kibana-plugin-plugins-data-public.iaggtype.md) | | | [IFieldFormat](./kibana-plugin-plugins-data-public.ifieldformat.md) | | @@ -162,6 +160,7 @@ | [ParsedInterval](./kibana-plugin-plugins-data-public.parsedinterval.md) | | | [PhraseFilter](./kibana-plugin-plugins-data-public.phrasefilter.md) | | | [PhrasesFilter](./kibana-plugin-plugins-data-public.phrasesfilter.md) | | +| [Query](./kibana-plugin-plugins-data-public.query.md) | | | [QueryStart](./kibana-plugin-plugins-data-public.querystart.md) | | | [QuerySuggestion](./kibana-plugin-plugins-data-public.querysuggestion.md) | \* | | [QuerySuggestionGetFn](./kibana-plugin-plugins-data-public.querysuggestiongetfn.md) | | @@ -173,4 +172,5 @@ | [TabbedAggRow](./kibana-plugin-plugins-data-public.tabbedaggrow.md) | \* | | [TimefilterContract](./kibana-plugin-plugins-data-public.timefiltercontract.md) | | | [TimeHistoryContract](./kibana-plugin-plugins-data-public.timehistorycontract.md) | | +| [TimeRange](./kibana-plugin-plugins-data-public.timerange.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.aggregatable.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.aggregatable.md new file mode 100644 index 0000000000000..2889ee34ad77b --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.aggregatable.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) > [aggregatable](./kibana-plugin-plugins-data-server.fielddescriptor.aggregatable.md) + +## FieldDescriptor.aggregatable property + +Signature: + +```typescript +aggregatable: boolean; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.estypes.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.estypes.md new file mode 100644 index 0000000000000..9caa374d8da48 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.estypes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) > [esTypes](./kibana-plugin-plugins-data-server.fielddescriptor.estypes.md) + +## FieldDescriptor.esTypes property + +Signature: + +```typescript +esTypes: string[]; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.md new file mode 100644 index 0000000000000..693de675da940 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) + +## FieldDescriptor interface + +Signature: + +```typescript +export interface FieldDescriptor +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [aggregatable](./kibana-plugin-plugins-data-server.fielddescriptor.aggregatable.md) | boolean | | +| [esTypes](./kibana-plugin-plugins-data-server.fielddescriptor.estypes.md) | string[] | | +| [name](./kibana-plugin-plugins-data-server.fielddescriptor.name.md) | string | | +| [readFromDocValues](./kibana-plugin-plugins-data-server.fielddescriptor.readfromdocvalues.md) | boolean | | +| [searchable](./kibana-plugin-plugins-data-server.fielddescriptor.searchable.md) | boolean | | +| [subType](./kibana-plugin-plugins-data-server.fielddescriptor.subtype.md) | FieldSubType | | +| [type](./kibana-plugin-plugins-data-server.fielddescriptor.type.md) | string | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.name.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.name.md new file mode 100644 index 0000000000000..178880a34cd4d --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) > [name](./kibana-plugin-plugins-data-server.fielddescriptor.name.md) + +## FieldDescriptor.name property + +Signature: + +```typescript +name: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.readfromdocvalues.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.readfromdocvalues.md new file mode 100644 index 0000000000000..b60dc5d0dfed0 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.readfromdocvalues.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) > [readFromDocValues](./kibana-plugin-plugins-data-server.fielddescriptor.readfromdocvalues.md) + +## FieldDescriptor.readFromDocValues property + +Signature: + +```typescript +readFromDocValues: boolean; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.searchable.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.searchable.md new file mode 100644 index 0000000000000..efc7b4219a355 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.searchable.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) > [searchable](./kibana-plugin-plugins-data-server.fielddescriptor.searchable.md) + +## FieldDescriptor.searchable property + +Signature: + +```typescript +searchable: boolean; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.subtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.subtype.md new file mode 100644 index 0000000000000..b08179f12f250 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.subtype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) > [subType](./kibana-plugin-plugins-data-server.fielddescriptor.subtype.md) + +## FieldDescriptor.subType property + +Signature: + +```typescript +subType?: FieldSubType; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.type.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.type.md new file mode 100644 index 0000000000000..7b0513a60c90e --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) > [type](./kibana-plugin-plugins-data-server.fielddescriptor.type.md) + +## FieldDescriptor.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.fieldformatmap.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.fieldformatmap.md deleted file mode 100644 index ab9e3171d7d7b..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.fieldformatmap.md +++ /dev/null @@ -1,14 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) > [fieldFormatMap](./kibana-plugin-plugins-data-server.iindexpattern.fieldformatmap.md) - -## IIndexPattern.fieldFormatMap property - -Signature: - -```typescript -fieldFormatMap?: Record; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.fields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.fields.md deleted file mode 100644 index fb6d046ff2174..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.fields.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) > [fields](./kibana-plugin-plugins-data-server.iindexpattern.fields.md) - -## IIndexPattern.fields property - -Signature: - -```typescript -fields: IFieldType[]; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.gettimefield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.gettimefield.md deleted file mode 100644 index a4d6abcf86a94..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.gettimefield.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) > [getTimeField](./kibana-plugin-plugins-data-server.iindexpattern.gettimefield.md) - -## IIndexPattern.getTimeField() method - -Signature: - -```typescript -getTimeField?(): IFieldType | undefined; -``` -Returns: - -`IFieldType | undefined` - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.id.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.id.md deleted file mode 100644 index cac263df0f9aa..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.id.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) > [id](./kibana-plugin-plugins-data-server.iindexpattern.id.md) - -## IIndexPattern.id property - -Signature: - -```typescript -id?: string; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.md deleted file mode 100644 index a79244a24acf5..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.md +++ /dev/null @@ -1,29 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) - -## IIndexPattern interface - -Signature: - -```typescript -export interface IIndexPattern -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [fieldFormatMap](./kibana-plugin-plugins-data-server.iindexpattern.fieldformatmap.md) | Record<string, {
id: string;
params: unknown;
}> | | -| [fields](./kibana-plugin-plugins-data-server.iindexpattern.fields.md) | IFieldType[] | | -| [id](./kibana-plugin-plugins-data-server.iindexpattern.id.md) | string | | -| [timeFieldName](./kibana-plugin-plugins-data-server.iindexpattern.timefieldname.md) | string | | -| [title](./kibana-plugin-plugins-data-server.iindexpattern.title.md) | string | | -| [type](./kibana-plugin-plugins-data-server.iindexpattern.type.md) | string | | - -## Methods - -| Method | Description | -| --- | --- | -| [getTimeField()](./kibana-plugin-plugins-data-server.iindexpattern.gettimefield.md) | | - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.timefieldname.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.timefieldname.md deleted file mode 100644 index 14cf514477da4..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.timefieldname.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) > [timeFieldName](./kibana-plugin-plugins-data-server.iindexpattern.timefieldname.md) - -## IIndexPattern.timeFieldName property - -Signature: - -```typescript -timeFieldName?: string; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.title.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.title.md deleted file mode 100644 index 119963d7ff95d..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.title.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) > [title](./kibana-plugin-plugins-data-server.iindexpattern.title.md) - -## IIndexPattern.title property - -Signature: - -```typescript -title: string; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.type.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.type.md deleted file mode 100644 index 6b89b71664b23..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) > [type](./kibana-plugin-plugins-data-server.iindexpattern.type.md) - -## IIndexPattern.type property - -Signature: - -```typescript -type?: string; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._constructor_.md new file mode 100644 index 0000000000000..d1d31f1f0428e --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._constructor_.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [(constructor)](./kibana-plugin-plugins-data-server.indexpattern._constructor_.md) + +## IndexPattern.(constructor) + +Constructs a new instance of the `IndexPattern` class + +Signature: + +```typescript +constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| id | string | undefined | | +| { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, } | IndexPatternDeps | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._fetchfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._fetchfields.md new file mode 100644 index 0000000000000..d1dabe59d4c45 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._fetchfields.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [\_fetchFields](./kibana-plugin-plugins-data-server.indexpattern._fetchfields.md) + +## IndexPattern.\_fetchFields() method + +Signature: + +```typescript +_fetchFields(): Promise; +``` +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md new file mode 100644 index 0000000000000..320698b05e323 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [addScriptedField](./kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md) + +## IndexPattern.addScriptedField() method + +Signature: + +```typescript +addScriptedField(name: string, script: string, fieldType: string | undefined, lang: string): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | +| script | string | | +| fieldType | string | undefined | | +| lang | string | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.create.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.create.md new file mode 100644 index 0000000000000..82367e79480f1 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.create.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [create](./kibana-plugin-plugins-data-server.indexpattern.create.md) + +## IndexPattern.create() method + +Signature: + +```typescript +create(allowOverride?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| allowOverride | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md new file mode 100644 index 0000000000000..77e5d112a3db2 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md) + +## IndexPattern.fieldFormatMap property + +Signature: + +```typescript +fieldFormatMap: any; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fields.md new file mode 100644 index 0000000000000..17a63be92fedf --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fields.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [fields](./kibana-plugin-plugins-data-server.indexpattern.fields.md) + +## IndexPattern.fields property + +Signature: + +```typescript +fields: IIndexPatternFieldList & { + toSpec: () => FieldSpec[]; + }; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldsfetcher.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldsfetcher.md new file mode 100644 index 0000000000000..31683934e2252 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldsfetcher.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [fieldsFetcher](./kibana-plugin-plugins-data-server.indexpattern.fieldsfetcher.md) + +## IndexPattern.fieldsFetcher property + +Signature: + +```typescript +fieldsFetcher: any; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.flattenhit.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.flattenhit.md new file mode 100644 index 0000000000000..3f4851daaf488 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.flattenhit.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [flattenHit](./kibana-plugin-plugins-data-server.indexpattern.flattenhit.md) + +## IndexPattern.flattenHit property + +Signature: + +```typescript +flattenHit: any; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formatfield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formatfield.md new file mode 100644 index 0000000000000..9019904cf2b65 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formatfield.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [formatField](./kibana-plugin-plugins-data-server.indexpattern.formatfield.md) + +## IndexPattern.formatField property + +Signature: + +```typescript +formatField: any; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formathit.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formathit.md new file mode 100644 index 0000000000000..0bfd7466fb3a5 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formathit.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [formatHit](./kibana-plugin-plugins-data-server.indexpattern.formathit.md) + +## IndexPattern.formatHit property + +Signature: + +```typescript +formatHit: any; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getaggregationrestrictions.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getaggregationrestrictions.md new file mode 100644 index 0000000000000..b655e779e4fa4 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getaggregationrestrictions.md @@ -0,0 +1,29 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getAggregationRestrictions](./kibana-plugin-plugins-data-server.indexpattern.getaggregationrestrictions.md) + +## IndexPattern.getAggregationRestrictions() method + +Signature: + +```typescript +getAggregationRestrictions(): Record> | undefined; +``` +Returns: + +`Record> | undefined` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md new file mode 100644 index 0000000000000..eab6ae9bf9033 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md @@ -0,0 +1,29 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getComputedFields](./kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md) + +## IndexPattern.getComputedFields() method + +Signature: + +```typescript +getComputedFields(): { + storedFields: string[]; + scriptFields: any; + docvalueFields: { + field: any; + format: string; + }[]; + }; +``` +Returns: + +`{ + storedFields: string[]; + scriptFields: any; + docvalueFields: { + field: any; + format: string; + }[]; + }` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldbyname.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldbyname.md new file mode 100644 index 0000000000000..712be3b72828a --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldbyname.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getFieldByName](./kibana-plugin-plugins-data-server.indexpattern.getfieldbyname.md) + +## IndexPattern.getFieldByName() method + +Signature: + +```typescript +getFieldByName(name: string): IndexPatternField | undefined; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | + +Returns: + +`IndexPatternField | undefined` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md new file mode 100644 index 0000000000000..3218187696918 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getFormatterForField](./kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md) + +## IndexPattern.getFormatterForField() method + +Signature: + +```typescript +getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| field | IndexPatternField | IndexPatternField['spec'] | IFieldType | | + +Returns: + +`FieldFormat` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md new file mode 100644 index 0000000000000..89d79d9b750fa --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getNonScriptedFields](./kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md) + +## IndexPattern.getNonScriptedFields() method + +Signature: + +```typescript +getNonScriptedFields(): IndexPatternField[]; +``` +Returns: + +`IndexPatternField[]` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md new file mode 100644 index 0000000000000..edfff8ec5efac --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getScriptedFields](./kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md) + +## IndexPattern.getScriptedFields() method + +Signature: + +```typescript +getScriptedFields(): IndexPatternField[]; +``` +Returns: + +`IndexPatternField[]` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md new file mode 100644 index 0000000000000..f463dcd29a2e4 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getSourceFiltering](./kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md) + +## IndexPattern.getSourceFiltering() method + +Signature: + +```typescript +getSourceFiltering(): { + excludes: any[]; + }; +``` +Returns: + +`{ + excludes: any[]; + }` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.gettimefield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.gettimefield.md new file mode 100644 index 0000000000000..b5806f883fb9f --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.gettimefield.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getTimeField](./kibana-plugin-plugins-data-server.indexpattern.gettimefield.md) + +## IndexPattern.getTimeField() method + +Signature: + +```typescript +getTimeField(): IndexPatternField | undefined; +``` +Returns: + +`IndexPatternField | undefined` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.id.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.id.md new file mode 100644 index 0000000000000..8fad82bd06705 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [id](./kibana-plugin-plugins-data-server.indexpattern.id.md) + +## IndexPattern.id property + +Signature: + +```typescript +id?: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.init.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.init.md new file mode 100644 index 0000000000000..bc17ff00cc9cf --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.init.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [init](./kibana-plugin-plugins-data-server.indexpattern.init.md) + +## IndexPattern.init() method + +Signature: + +```typescript +init(): Promise; +``` +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.initfromspec.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.initfromspec.md new file mode 100644 index 0000000000000..6fbf621254ff3 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.initfromspec.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [initFromSpec](./kibana-plugin-plugins-data-server.indexpattern.initfromspec.md) + +## IndexPattern.initFromSpec() method + +Signature: + +```typescript +initFromSpec(spec: IndexPatternSpec): this; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| spec | IndexPatternSpec | | + +Returns: + +`this` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.intervalname.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.intervalname.md new file mode 100644 index 0000000000000..caaa6929235f8 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.intervalname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [intervalName](./kibana-plugin-plugins-data-server.indexpattern.intervalname.md) + +## IndexPattern.intervalName property + +Signature: + +```typescript +intervalName: string | undefined; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimebased.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimebased.md new file mode 100644 index 0000000000000..790744979942d --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimebased.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [isTimeBased](./kibana-plugin-plugins-data-server.indexpattern.istimebased.md) + +## IndexPattern.isTimeBased() method + +Signature: + +```typescript +isTimeBased(): boolean; +``` +Returns: + +`boolean` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimebasedwildcard.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimebasedwildcard.md new file mode 100644 index 0000000000000..7ef5e8318040a --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimebasedwildcard.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [isTimeBasedWildcard](./kibana-plugin-plugins-data-server.indexpattern.istimebasedwildcard.md) + +## IndexPattern.isTimeBasedWildcard() method + +Signature: + +```typescript +isTimeBasedWildcard(): boolean; +``` +Returns: + +`boolean` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimenanosbased.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimenanosbased.md new file mode 100644 index 0000000000000..22fb60eba4f6e --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimenanosbased.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [isTimeNanosBased](./kibana-plugin-plugins-data-server.indexpattern.istimenanosbased.md) + +## IndexPattern.isTimeNanosBased() method + +Signature: + +```typescript +isTimeNanosBased(): boolean; +``` +Returns: + +`boolean` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.iswildcard.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.iswildcard.md new file mode 100644 index 0000000000000..e0adf71b45efa --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.iswildcard.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [isWildcard](./kibana-plugin-plugins-data-server.indexpattern.iswildcard.md) + +## IndexPattern.isWildcard() method + +Signature: + +```typescript +isWildcard(): boolean; +``` +Returns: + +`boolean` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md new file mode 100644 index 0000000000000..27d7dd0315753 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md @@ -0,0 +1,66 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) + +## IndexPattern class + +Signature: + +```typescript +export declare class IndexPattern implements IIndexPattern +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(id, { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, })](./kibana-plugin-plugins-data-server.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md) | | any | | +| [fields](./kibana-plugin-plugins-data-server.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => FieldSpec[];
} | | +| [fieldsFetcher](./kibana-plugin-plugins-data-server.indexpattern.fieldsfetcher.md) | | any | | +| [flattenHit](./kibana-plugin-plugins-data-server.indexpattern.flattenhit.md) | | any | | +| [formatField](./kibana-plugin-plugins-data-server.indexpattern.formatfield.md) | | any | | +| [formatHit](./kibana-plugin-plugins-data-server.indexpattern.formathit.md) | | any | | +| [id](./kibana-plugin-plugins-data-server.indexpattern.id.md) | | string | | +| [intervalName](./kibana-plugin-plugins-data-server.indexpattern.intervalname.md) | | string | undefined | | +| [metaFields](./kibana-plugin-plugins-data-server.indexpattern.metafields.md) | | string[] | | +| [originalBody](./kibana-plugin-plugins-data-server.indexpattern.originalbody.md) | | {
[key: string]: any;
} | | +| [sourceFilters](./kibana-plugin-plugins-data-server.indexpattern.sourcefilters.md) | | SourceFilter[] | | +| [timeFieldName](./kibana-plugin-plugins-data-server.indexpattern.timefieldname.md) | | string | undefined | | +| [title](./kibana-plugin-plugins-data-server.indexpattern.title.md) | | string | | +| [type](./kibana-plugin-plugins-data-server.indexpattern.type.md) | | string | undefined | | +| [typeMeta](./kibana-plugin-plugins-data-server.indexpattern.typemeta.md) | | TypeMeta | | +| [version](./kibana-plugin-plugins-data-server.indexpattern.version.md) | | string | undefined | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [\_fetchFields()](./kibana-plugin-plugins-data-server.indexpattern._fetchfields.md) | | | +| [addScriptedField(name, script, fieldType, lang)](./kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md) | | | +| [create(allowOverride)](./kibana-plugin-plugins-data-server.indexpattern.create.md) | | | +| [getAggregationRestrictions()](./kibana-plugin-plugins-data-server.indexpattern.getaggregationrestrictions.md) | | | +| [getComputedFields()](./kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md) | | | +| [getFieldByName(name)](./kibana-plugin-plugins-data-server.indexpattern.getfieldbyname.md) | | | +| [getFormatterForField(field)](./kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md) | | | +| [getNonScriptedFields()](./kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md) | | | +| [getScriptedFields()](./kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md) | | | +| [getSourceFiltering()](./kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md) | | | +| [getTimeField()](./kibana-plugin-plugins-data-server.indexpattern.gettimefield.md) | | | +| [init()](./kibana-plugin-plugins-data-server.indexpattern.init.md) | | | +| [initFromSpec(spec)](./kibana-plugin-plugins-data-server.indexpattern.initfromspec.md) | | | +| [isTimeBased()](./kibana-plugin-plugins-data-server.indexpattern.istimebased.md) | | | +| [isTimeBasedWildcard()](./kibana-plugin-plugins-data-server.indexpattern.istimebasedwildcard.md) | | | +| [isTimeNanosBased()](./kibana-plugin-plugins-data-server.indexpattern.istimenanosbased.md) | | | +| [isWildcard()](./kibana-plugin-plugins-data-server.indexpattern.iswildcard.md) | | | +| [popularizeField(fieldName, unit)](./kibana-plugin-plugins-data-server.indexpattern.popularizefield.md) | | | +| [prepBody()](./kibana-plugin-plugins-data-server.indexpattern.prepbody.md) | | | +| [refreshFields()](./kibana-plugin-plugins-data-server.indexpattern.refreshfields.md) | | | +| [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md) | | | +| [toSpec()](./kibana-plugin-plugins-data-server.indexpattern.tospec.md) | | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.metafields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.metafields.md new file mode 100644 index 0000000000000..a2c7c806d6057 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.metafields.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [metaFields](./kibana-plugin-plugins-data-server.indexpattern.metafields.md) + +## IndexPattern.metaFields property + +Signature: + +```typescript +metaFields: string[]; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.originalbody.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.originalbody.md new file mode 100644 index 0000000000000..b7357d6e85ae7 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.originalbody.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [originalBody](./kibana-plugin-plugins-data-server.indexpattern.originalbody.md) + +## IndexPattern.originalBody property + +Signature: + +```typescript +originalBody: { + [key: string]: any; + }; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.popularizefield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.popularizefield.md new file mode 100644 index 0000000000000..8b2c9242a6256 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.popularizefield.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [popularizeField](./kibana-plugin-plugins-data-server.indexpattern.popularizefield.md) + +## IndexPattern.popularizeField() method + +Signature: + +```typescript +popularizeField(fieldName: string, unit?: number): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| fieldName | string | | +| unit | number | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.prepbody.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.prepbody.md new file mode 100644 index 0000000000000..0de4418da46f8 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.prepbody.md @@ -0,0 +1,33 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [prepBody](./kibana-plugin-plugins-data-server.indexpattern.prepbody.md) + +## IndexPattern.prepBody() method + +Signature: + +```typescript +prepBody(): { + title: string; + timeFieldName: string | undefined; + intervalName: string | undefined; + sourceFilters: string | undefined; + fields: string | undefined; + fieldFormatMap: string | undefined; + type: string | undefined; + typeMeta: string | undefined; + }; +``` +Returns: + +`{ + title: string; + timeFieldName: string | undefined; + intervalName: string | undefined; + sourceFilters: string | undefined; + fields: string | undefined; + fieldFormatMap: string | undefined; + type: string | undefined; + typeMeta: string | undefined; + }` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.refreshfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.refreshfields.md new file mode 100644 index 0000000000000..168e131937eea --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.refreshfields.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [refreshFields](./kibana-plugin-plugins-data-server.indexpattern.refreshfields.md) + +## IndexPattern.refreshFields() method + +Signature: + +```typescript +refreshFields(): Promise; +``` +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md new file mode 100644 index 0000000000000..8205175485398 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [removeScriptedField](./kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md) + +## IndexPattern.removeScriptedField() method + +Signature: + +```typescript +removeScriptedField(fieldName: string): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| fieldName | string | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.sourcefilters.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.sourcefilters.md new file mode 100644 index 0000000000000..d359bef2f30a9 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.sourcefilters.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [sourceFilters](./kibana-plugin-plugins-data-server.indexpattern.sourcefilters.md) + +## IndexPattern.sourceFilters property + +Signature: + +```typescript +sourceFilters?: SourceFilter[]; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.timefieldname.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.timefieldname.md new file mode 100644 index 0000000000000..35740afa4e3dc --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.timefieldname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [timeFieldName](./kibana-plugin-plugins-data-server.indexpattern.timefieldname.md) + +## IndexPattern.timeFieldName property + +Signature: + +```typescript +timeFieldName: string | undefined; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.title.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.title.md new file mode 100644 index 0000000000000..4cebde989aebd --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.title.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [title](./kibana-plugin-plugins-data-server.indexpattern.title.md) + +## IndexPattern.title property + +Signature: + +```typescript +title: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.tospec.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.tospec.md new file mode 100644 index 0000000000000..5d76b8f00853b --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.tospec.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [toSpec](./kibana-plugin-plugins-data-server.indexpattern.tospec.md) + +## IndexPattern.toSpec() method + +Signature: + +```typescript +toSpec(): IndexPatternSpec; +``` +Returns: + +`IndexPatternSpec` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.type.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.type.md new file mode 100644 index 0000000000000..01154ab5444d1 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [type](./kibana-plugin-plugins-data-server.indexpattern.type.md) + +## IndexPattern.type property + +Signature: + +```typescript +type: string | undefined; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.typemeta.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.typemeta.md new file mode 100644 index 0000000000000..b16bcec404d97 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.typemeta.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [typeMeta](./kibana-plugin-plugins-data-server.indexpattern.typemeta.md) + +## IndexPattern.typeMeta property + +Signature: + +```typescript +typeMeta?: TypeMeta; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.version.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.version.md new file mode 100644 index 0000000000000..e4297d8389111 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.version.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [version](./kibana-plugin-plugins-data-server.indexpattern.version.md) + +## IndexPattern.version property + +Signature: + +```typescript +version: string | undefined; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.aggregatable.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.aggregatable.md deleted file mode 100644 index 92994b851ec85..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.aggregatable.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) > [aggregatable](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.aggregatable.md) - -## IndexPatternFieldDescriptor.aggregatable property - -Signature: - -```typescript -aggregatable: boolean; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.estypes.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.estypes.md deleted file mode 100644 index f24ba9a48d85e..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.estypes.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) > [esTypes](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.estypes.md) - -## IndexPatternFieldDescriptor.esTypes property - -Signature: - -```typescript -esTypes: string[]; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md deleted file mode 100644 index d84d0cba06ac6..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) - -## IndexPatternFieldDescriptor interface - -Signature: - -```typescript -export interface FieldDescriptor -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [aggregatable](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.aggregatable.md) | boolean | | -| [esTypes](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.estypes.md) | string[] | | -| [name](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.name.md) | string | | -| [readFromDocValues](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.readfromdocvalues.md) | boolean | | -| [searchable](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.searchable.md) | boolean | | -| [subType](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.subtype.md) | FieldSubType | | -| [type](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.type.md) | string | | - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.name.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.name.md deleted file mode 100644 index 16ea60c5b8ae2..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.name.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) > [name](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.name.md) - -## IndexPatternFieldDescriptor.name property - -Signature: - -```typescript -name: string; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.readfromdocvalues.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.readfromdocvalues.md deleted file mode 100644 index fc8667196c879..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.readfromdocvalues.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) > [readFromDocValues](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.readfromdocvalues.md) - -## IndexPatternFieldDescriptor.readFromDocValues property - -Signature: - -```typescript -readFromDocValues: boolean; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.searchable.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.searchable.md deleted file mode 100644 index 7d159c65b40bd..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.searchable.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) > [searchable](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.searchable.md) - -## IndexPatternFieldDescriptor.searchable property - -Signature: - -```typescript -searchable: boolean; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.subtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.subtype.md deleted file mode 100644 index 7053eaf08138c..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.subtype.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) > [subType](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.subtype.md) - -## IndexPatternFieldDescriptor.subType property - -Signature: - -```typescript -subType?: FieldSubType; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.type.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.type.md deleted file mode 100644 index bb571d1bee14a..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) > [type](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.type.md) - -## IndexPatternFieldDescriptor.type property - -Signature: - -```typescript -type: string; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 3c477e17503f4..dea79f5dc4a9f 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -9,6 +9,7 @@ | Class | Description | | --- | --- | | [AggParamType](./kibana-plugin-plugins-data-server.aggparamtype.md) | | +| [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) | | | [IndexPatternsFetcher](./kibana-plugin-plugins-data-server.indexpatternsfetcher.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) | | | [Plugin](./kibana-plugin-plugins-data-server.plugin.md) | | @@ -41,14 +42,13 @@ | --- | --- | | [AggParamOption](./kibana-plugin-plugins-data-server.aggparamoption.md) | | | [EsQueryConfig](./kibana-plugin-plugins-data-server.esqueryconfig.md) | | +| [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) | | | [FieldFormatConfig](./kibana-plugin-plugins-data-server.fieldformatconfig.md) | | | [IEsSearchRequest](./kibana-plugin-plugins-data-server.iessearchrequest.md) | | | [IEsSearchResponse](./kibana-plugin-plugins-data-server.iessearchresponse.md) | | | [IFieldSubType](./kibana-plugin-plugins-data-server.ifieldsubtype.md) | | | [IFieldType](./kibana-plugin-plugins-data-server.ifieldtype.md) | | -| [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) | | | [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) | Use data plugin interface instead | -| [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) | | | [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) | | | [ISearchSetup](./kibana-plugin-plugins-data-server.isearchsetup.md) | | | [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) | | diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index 7f72bfe55c7cd..b83323ea19556 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -152,7 +152,7 @@ export class IndexPatternField implements IFieldType { getFormatterForField, }: { getFormatterForField?: IndexPattern['getFormatterForField']; - } = {}) { + } = {}): FieldSpec { return { count: this.count, script: this.script, diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 76f1a5e59d0ee..b2ecca04ca8df 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -28,6 +28,7 @@ import { IIndexPattern, FieldTypeUnknownError, FieldFormatNotFoundError, + IFieldType, } from '../../../common'; import { findByTitle } from '../utils'; import { IndexPatternMissingIndices } from '../lib'; @@ -485,7 +486,9 @@ export class IndexPattern implements IIndexPattern { }; } - getFormatterForField(field: IndexPatternField | IndexPatternField['spec']): FieldFormat { + getFormatterForField( + field: IndexPatternField | IndexPatternField['spec'] | IFieldType + ): FieldFormat { return ( this.fieldFormatMap[field.name] || this.fieldFormats.getDefaultInstance( diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 7a230c20f6cd0..2264c5e3aaf56 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -22,16 +22,18 @@ import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notificatio import type { SavedObject } from 'src/core/server'; import { IFieldType } from './fields'; import { SerializedFieldFormat } from '../../../expressions/common'; -import { KBN_FIELD_TYPES } from '..'; +import { KBN_FIELD_TYPES, IndexPatternField, FieldFormat } from '..'; export interface IIndexPattern { - [key: string]: any; fields: IFieldType[]; title: string; id?: string; type?: string; timeFieldName?: string; getTimeField?(): IFieldType | undefined; + getFormatterForField?: ( + field: IndexPatternField | IndexPatternField['spec'] | IFieldType + ) => FieldFormat; fieldFormatMap?: Record< string, { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index db8d9dba4e0c7..b2f1cc19d0f90 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1004,8 +1004,6 @@ export interface IFieldType { // // @public (undocumented) export interface IIndexPattern { - // (undocumented) - [key: string]: any; // (undocumented) fieldFormatMap?: Record FieldFormat; + // (undocumented) getTimeField?(): IFieldType | undefined; // (undocumented) id?: string; @@ -1121,7 +1121,7 @@ export class IndexPattern implements IIndexPattern { // (undocumented) getFieldByName(name: string): IndexPatternField | undefined; // (undocumented) - getFormatterForField(field: IndexPatternField | IndexPatternField['spec']): FieldFormat; + getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat; // (undocumented) getNonScriptedFields(): IndexPatternField[]; // (undocumented) @@ -1282,24 +1282,7 @@ export class IndexPatternField implements IFieldType { // (undocumented) toSpec({ getFormatterForField, }?: { getFormatterForField?: IndexPattern['getFormatterForField']; - }): { - count: number; - script: string | undefined; - lang: string | undefined; - conflictDescriptions: Record | undefined; - name: string; - type: string; - esTypes: string[] | undefined; - scripted: boolean; - searchable: boolean; - aggregatable: boolean; - readFromDocValues: boolean; - subType: import("../types").IFieldSubType | undefined; - format: { - id: any; - params: any; - } | undefined; - }; + }): FieldSpec; // (undocumented) get type(): string; // (undocumented) diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 03baff4910309..9be8ef1b53423 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -133,16 +133,17 @@ export { IndexPatternsFetcher, FieldDescriptor as IndexPatternFieldDescriptor, shouldReadFieldFromDocValues, // used only in logstash_fields fixture + FieldDescriptor, } from './index_patterns'; export { - IIndexPattern, IFieldType, IFieldSubType, ES_FIELD_TYPES, KBN_FIELD_TYPES, IndexPatternAttributes, UI_SETTINGS, + IndexPattern, } from '../common'; /** diff --git a/src/plugins/data/server/index_patterns/utils.ts b/src/plugins/data/server/index_patterns/utils.ts index e841097fe49c2..1e7a85599612c 100644 --- a/src/plugins/data/server/index_patterns/utils.ts +++ b/src/plugins/data/server/index_patterns/utils.ts @@ -18,11 +18,11 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { IIndexPattern, IFieldType } from '../../common'; +import { IFieldType, IndexPatternAttributes, SavedObject } from '../../common'; export const getFieldByName = ( fieldName: string, - indexPattern: IIndexPattern + indexPattern: SavedObject ): IFieldType | undefined => { const fields: IFieldType[] = indexPattern && JSON.parse(indexPattern.attributes.fields); const field = fields && fields.find((f) => f.name === fieldName); @@ -33,8 +33,8 @@ export const getFieldByName = ( export const findIndexPatternById = async ( savedObjectsClient: SavedObjectsClientContract, index: string -): Promise => { - const savedObjectsResponse = await savedObjectsClient.find({ +): Promise | undefined> => { + const savedObjectsResponse = await savedObjectsClient.find({ type: 'index-pattern', fields: ['fields'], search: `"${index}"`, @@ -42,6 +42,6 @@ export const findIndexPatternById = async ( }); if (savedObjectsResponse.total > 0) { - return (savedObjectsResponse.saved_objects[0] as unknown) as IIndexPattern; + return savedObjectsResponse.saved_objects[0]; } }; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 2024e9e7f2974..05b99c754edf7 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -396,6 +396,32 @@ export interface EsQueryConfig { queryStringOptions: Record; } +// Warning: (ae-missing-release-tag) "FieldDescriptor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface FieldDescriptor { + // (undocumented) + aggregatable: boolean; + // (undocumented) + esTypes: string[]; + // (undocumented) + name: string; + // (undocumented) + readFromDocValues: boolean; + // (undocumented) + searchable: boolean; + // Warning: (ae-forgotten-export) The symbol "FieldSubType" needs to be exported by the entry point index.d.ts + // + // (undocumented) + subType?: FieldSubType; + // (undocumented) + type: string; +} + +export { FieldDescriptor } + +export { FieldDescriptor as IndexPatternFieldDescriptor } + // Warning: (ae-missing-release-tag) "FieldFormatConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -467,6 +493,7 @@ export function getShardTimeout(config: SharedGlobalConfig): { timeout?: undefined; }; +// Warning: (ae-forgotten-export) The symbol "IIndexPattern" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "getTime" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -590,37 +617,135 @@ export interface IFieldType { visualizable?: boolean; } -// Warning: (ae-missing-release-tag) "IIndexPattern" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-forgotten-export) The symbol "MetricAggType" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "IMetricAggType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type IMetricAggType = MetricAggType; + +// Warning: (ae-missing-release-tag) "IndexPattern" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface IIndexPattern { +export class IndexPattern implements IIndexPattern { + // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts + constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps); // (undocumented) - [key: string]: any; + addScriptedField(name: string, script: string, fieldType: string | undefined, lang: string): Promise; + // (undocumented) + create(allowOverride?: boolean): Promise; + // (undocumented) + _fetchFields(): Promise; + // (undocumented) + fieldFormatMap: any; + // Warning: (ae-forgotten-export) The symbol "IIndexPatternFieldList" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fields: IIndexPatternFieldList & { + toSpec: () => FieldSpec[]; + }; + // (undocumented) + fieldsFetcher: any; + // (undocumented) + flattenHit: any; // (undocumented) - fieldFormatMap?: Record; + formatField: any; // (undocumented) - fields: IFieldType[]; + formatHit: any; // (undocumented) - getTimeField?(): IFieldType | undefined; + getAggregationRestrictions(): Record> | undefined; + // (undocumented) + getComputedFields(): { + storedFields: string[]; + scriptFields: any; + docvalueFields: { + field: any; + format: string; + }[]; + }; + // (undocumented) + getFieldByName(name: string): IndexPatternField | undefined; + // (undocumented) + getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat; + // Warning: (ae-forgotten-export) The symbol "IndexPatternField" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getNonScriptedFields(): IndexPatternField[]; + // (undocumented) + getScriptedFields(): IndexPatternField[]; + // (undocumented) + getSourceFiltering(): { + excludes: any[]; + }; + // (undocumented) + getTimeField(): IndexPatternField | undefined; // (undocumented) id?: string; // (undocumented) - timeFieldName?: string; + init(): Promise; + // Warning: (ae-forgotten-export) The symbol "IndexPatternSpec" needs to be exported by the entry point index.d.ts + // + // (undocumented) + initFromSpec(spec: IndexPatternSpec): this; + // (undocumented) + intervalName: string | undefined; + // (undocumented) + isTimeBased(): boolean; + // (undocumented) + isTimeBasedWildcard(): boolean; + // (undocumented) + isTimeNanosBased(): boolean; + // (undocumented) + isWildcard(): boolean; + // (undocumented) + metaFields: string[]; + // (undocumented) + originalBody: { + [key: string]: any; + }; + // (undocumented) + popularizeField(fieldName: string, unit?: number): Promise; + // (undocumented) + prepBody(): { + title: string; + timeFieldName: string | undefined; + intervalName: string | undefined; + sourceFilters: string | undefined; + fields: string | undefined; + fieldFormatMap: string | undefined; + type: string | undefined; + typeMeta: string | undefined; + }; + // (undocumented) + refreshFields(): Promise; + // (undocumented) + removeScriptedField(fieldName: string): void; + // Warning: (ae-forgotten-export) The symbol "SourceFilter" needs to be exported by the entry point index.d.ts + // + // (undocumented) + sourceFilters?: SourceFilter[]; + // (undocumented) + timeFieldName: string | undefined; // (undocumented) title: string; // (undocumented) - type?: string; + toSpec(): IndexPatternSpec; + // (undocumented) + type: string | undefined; + // Warning: (ae-forgotten-export) The symbol "TypeMeta" needs to be exported by the entry point index.d.ts + // + // (undocumented) + typeMeta?: TypeMeta; + // (undocumented) + version: string | undefined; } -// Warning: (ae-forgotten-export) The symbol "MetricAggType" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "IMetricAggType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type IMetricAggType = MetricAggType; - // Warning: (ae-missing-release-tag) "IndexPatternAttributes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @deprecated @@ -643,28 +768,6 @@ export interface IndexPatternAttributes { typeMeta: string; } -// Warning: (ae-missing-release-tag) "FieldDescriptor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface IndexPatternFieldDescriptor { - // (undocumented) - aggregatable: boolean; - // (undocumented) - esTypes: string[]; - // (undocumented) - name: string; - // (undocumented) - readFromDocValues: boolean; - // (undocumented) - searchable: boolean; - // Warning: (ae-forgotten-export) The symbol "FieldSubType" needs to be exported by the entry point index.d.ts - // - // (undocumented) - subType?: FieldSubType; - // (undocumented) - type: string; -} - // Warning: (ae-missing-release-tag) "indexPatterns" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -683,14 +786,14 @@ export class IndexPatternsFetcher { metaFields: string[]; lookBack: number; interval: string; - }): Promise; + }): Promise; getFieldsForWildcard(options: { pattern: string | string[]; metaFields?: string[]; fieldCapsOptions?: { allowNoIndices: boolean; }; - }): Promise; + }): Promise; } // Warning: (ae-missing-release-tag) "ISearchOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1113,7 +1216,6 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // // src/plugins/data/common/es_query/filters/meta_filter.ts:53:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/fields/types.ts:41:25 - (ae-forgotten-export) The symbol "IndexPattern" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts @@ -1135,19 +1237,19 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:225:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:225:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:225:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:225:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:227:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:228:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:237:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:238:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:243:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:244:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:248:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:251:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:226:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:226:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:226:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:226:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:228:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:229:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:238:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:239:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:240:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:244:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:245:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:249:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:252:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:88:66 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover/public/application/components/discover_legacy.tsx index 1a98843649259..9c3d833d73b23 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.tsx +++ b/src/plugins/discover/public/application/components/discover_legacy.tsx @@ -25,7 +25,7 @@ import { IUiSettingsClient, MountPoint } from 'kibana/public'; import { HitsCounter } from './hits_counter'; import { TimechartHeader } from './timechart_header'; import { DiscoverSidebar } from './sidebar'; -import { getServices, IIndexPattern } from '../../kibana_services'; +import { getServices, IndexPattern } from '../../kibana_services'; // @ts-ignore import { DiscoverNoResults } from '../angular/directives/no_results'; import { DiscoverUninitialized } from '../angular/directives/uninitialized'; @@ -58,7 +58,7 @@ export interface DiscoverLegacyProps { fieldCounts: Record; histogramData: Chart; hits: number; - indexPattern: IIndexPattern; + indexPattern: IndexPattern; minimumVisibleRows: number; onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; onChangeInterval: (interval: string) => void; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx index 319b9b2b3fce2..e7ac1af7c1be8 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { IndexPatternField, IIndexPattern } from 'src/plugins/data/public'; +import { IndexPatternField, IndexPattern } from 'src/plugins/data/public'; import { IndexedFieldsTable } from './indexed_fields_table'; jest.mock('@elastic/eui', () => ({ @@ -43,7 +43,7 @@ const helpers = { const indexPattern = ({ getNonScriptedFields: () => fields, -} as unknown) as IIndexPattern; +} as unknown) as IndexPattern; const mockFieldToIndexPatternField = (spec: Record) => { return new IndexPatternField( diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 23977aac7fa7a..7be420e2af50d 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -19,23 +19,19 @@ import React, { Component } from 'react'; import { createSelector } from 'reselect'; -import { - IndexPatternField, - IIndexPattern, - IFieldType, -} from '../../../../../../plugins/data/public'; +import { IndexPatternField, IndexPattern, IFieldType } from '../../../../../../plugins/data/public'; import { Table } from './components/table'; import { getFieldFormat } from './lib'; import { IndexedFieldItem } from './types'; interface IndexedFieldsTableProps { fields: IndexPatternField[]; - indexPattern: IIndexPattern; + indexPattern: IndexPattern; fieldFilter?: string; indexedFieldTypeFilter?: string; helpers: { redirectToRoute: (obj: any) => void; - getFieldInfo: (indexPattern: IIndexPattern, field: IFieldType) => string[]; + getFieldInfo: (indexPattern: IndexPattern, field: IFieldType) => string[]; }; fieldWildcardMatcher: (filters: any[]) => (val: any) => boolean; } diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx index e43ee2e55eeca..2d3a61b42c3a4 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx @@ -22,13 +22,13 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { Table, TableProps, TableState } from './table'; import { EuiTableFieldDataColumnType, keys } from '@elastic/eui'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { IndexPattern } from 'src/plugins/data/public'; import { SourceFiltersTableFilter } from '../../types'; -const indexPattern = {} as IIndexPattern; +const indexPattern = {} as IndexPattern; const items: SourceFiltersTableFilter[] = [{ value: 'tim*', clientId: '' }]; -const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as IIndexPattern); +const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as IndexPattern); const getTableColumnRender = ( component: ShallowWrapper, diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx index f73d756f28116..c5b09961f25fc 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx @@ -30,7 +30,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { IndexPattern } from 'src/plugins/data/public'; import { SourceFiltersTableFilter } from '../../types'; const filterHeader = i18n.translate( @@ -80,7 +80,7 @@ const cancelAria = i18n.translate( ); export interface TableProps { - indexPattern: IIndexPattern; + indexPattern: IndexPattern; items: SourceFiltersTableFilter[]; deleteFilter: Function; fieldWildcardMatcher: Function; diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index cb30c6c064848..49030dc8cacc5 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -8,7 +8,7 @@ import LRU from 'lru-cache'; import { LegacyAPICaller } from '../../../../../../src/core/server'; import { IndexPatternsFetcher, - IIndexPattern, + FieldDescriptor, } from '../../../../../../src/plugins/data/server'; import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; import { @@ -17,7 +17,12 @@ import { } from '../../../common/processor_event'; import { APMRequestHandlerContext } from '../../routes/typings'; -const cache = new LRU({ +interface IndexPatternTitleAndFields { + title: string; + fields: FieldDescriptor[]; +} + +const cache = new LRU({ max: 100, maxAge: 1000 * 60, }); @@ -53,7 +58,7 @@ export const getDynamicIndexPattern = async ({ pattern: patternIndices, }); - const indexPattern: IIndexPattern = { + const indexPattern: IndexPatternTitleAndFields = { fields, title: indexPatternTitle, }; diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts index 8688bbe549f51..2af6413da039b 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -6,11 +6,12 @@ import _ from 'lodash'; import { + SavedObject, SavedObjectAttribute, SavedObjectAttributes, SavedObjectsClientContract, } from 'kibana/server'; -import { IFieldType, IIndexPattern } from 'src/plugins/data/public'; +import { IFieldType, IndexPatternAttributes } from 'src/plugins/data/public'; import { ES_GEO_FIELD_TYPE, LAYER_TYPE, @@ -64,7 +65,9 @@ function getUniqueLayerCounts(layerCountsList: ILayerTypeCount[], mapsCount: num }, {}); } -function getIndexPatternsWithGeoFieldCount(indexPatterns: IIndexPattern[]) { +function getIndexPatternsWithGeoFieldCount( + indexPatterns: Array> +) { const fieldLists = indexPatterns.map((indexPattern) => indexPattern.attributes && indexPattern.attributes.fields ? JSON.parse(indexPattern.attributes.fields) @@ -112,7 +115,7 @@ function getEMSLayerCount(layerLists: LayerDescriptor[][]): ILayerTypeCount[] { } function isFieldGeoShape( - indexPatterns: IIndexPattern[], + indexPatterns: Array>, indexPatternId: string, geoField: string | undefined ): boolean { @@ -120,9 +123,11 @@ function isFieldGeoShape( return false; } - const matchIndexPattern = indexPatterns.find((indexPattern: IIndexPattern) => { - return indexPattern.id === indexPatternId; - }); + const matchIndexPattern = indexPatterns.find( + (indexPattern: SavedObject) => { + return indexPattern.id === indexPatternId; + } + ); if (!matchIndexPattern) { return false; @@ -140,7 +145,10 @@ function isFieldGeoShape( return !!matchField && matchField.type === ES_GEO_FIELD_TYPE.GEO_SHAPE; } -function isGeoShapeAggLayer(indexPatterns: IIndexPattern[], layer: LayerDescriptor): boolean { +function isGeoShapeAggLayer( + indexPatterns: Array>, + layer: LayerDescriptor +): boolean { if (layer.sourceDescriptor === null) { return false; } @@ -176,7 +184,7 @@ function isGeoShapeAggLayer(indexPatterns: IIndexPattern[], layer: LayerDescript function getGeoShapeAggCount( layerLists: LayerDescriptor[][], - indexPatterns: IIndexPattern[] + indexPatterns: Array> ): number { const countsPerMap: number[] = layerLists.map((layerList: LayerDescriptor[]) => { const geoShapeAggLayers = layerList.filter((layerDescriptor) => { @@ -204,7 +212,7 @@ export function buildMapsTelemetry({ settings, }: { mapSavedObjects: MapSavedObject[]; - indexPatternSavedObjects: IIndexPattern[]; + indexPatternSavedObjects: Array>; settings: SavedObjectAttribute; }): SavedObjectAttributes { const layerLists: LayerDescriptor[][] = getLayerLists(mapSavedObjects); @@ -283,10 +291,12 @@ export async function getMapsTelemetry(config: MapsConfigType) { const savedObjectsClient = getInternalRepository(); // @ts-ignore const mapSavedObjects: MapSavedObject[] = await getMapSavedObjects(savedObjectsClient); - const indexPatternSavedObjects: IIndexPattern[] = (await getIndexPatternSavedObjects( + const indexPatternSavedObjects: Array> = (await getIndexPatternSavedObjects( // @ts-ignore savedObjectsClient - )) as IIndexPattern[]; + )) as Array>; const settings: SavedObjectAttribute = { showMapVisualizationTypes: config.showMapVisualizationTypes, }; diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts index d1a4df768a6ae..394dff1408134 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts @@ -5,13 +5,13 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { IIndexPattern } from 'src/plugins/data/server'; +import { IndexPatternAttributes } from 'src/plugins/data/server'; export class IndexPatternHandler { constructor(private savedObjectsClient: SavedObjectsClientContract) {} // returns a id based on an index pattern name async getIndexPatternId(indexName: string) { - const response = await this.savedObjectsClient.find({ + const response = await this.savedObjectsClient.find({ type: 'index-pattern', perPage: 10, search: `"${indexName}"`, diff --git a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts index 1fe64a25ebcaa..513f38122527b 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_from_savedobject/lib/get_csv_job.ts @@ -6,12 +6,7 @@ import { IUiSettingsClient, SavedObjectsClientContract } from 'kibana/server'; import { EsQueryConfig } from 'src/plugins/data/server'; -import { - esQuery, - Filter, - IIndexPattern, - Query, -} from '../../../../../../../src/plugins/data/server'; +import { esQuery, Filter, Query } from '../../../../../../../src/plugins/data/server'; import { TimeRangeParams } from '../../common'; import { GenerateCsvParams } from '../../csv/generate_csv'; import { @@ -126,7 +121,9 @@ export const getGenerateCsvParams = async ( _source: { includes }, docvalue_fields: docValueFields, query: esQuery.buildEsQuery( - indexPatternSavedObject as IIndexPattern, + // compromise made while factoring out IIndexPattern type + // @ts-expect-error + indexPatternSavedObject, (searchSourceQuery as unknown) as Query, (combinedFilter as unknown) as Filter, esQueryConfig diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx index f2eb5cf5b94f3..2ce9d1ea68b3c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.test.tsx @@ -13,6 +13,7 @@ import { esFilters, FilterManager, UI_SETTINGS, + IndexPattern, } from '../../../../../../../../src/plugins/data/public'; import { SeverityBadge } from '../severity_badge'; @@ -140,11 +141,11 @@ describe('helpers', () => { filterManager: mockFilterManager, query: mockQueryBarWithFilters.query, savedId: mockQueryBarWithFilters.saved_id, - indexPatterns: { + indexPatterns: ({ fields: [{ name: 'event.category', type: 'test type' }], title: 'test title', getFormatterForField: () => ({ convert: (val: unknown) => val }), - }, + } as unknown) as IndexPattern, }); const wrapper = shallow(result[0].description as React.ReactElement); const filterLabelComponent = wrapper.find(esFilters.FilterLabel).at(0); diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index b9dc7945e9579..3fc26811d46eb 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import Mustache from 'mustache'; import { UptimeAlertTypeFactory } from './types'; -import { esKuery, IIndexPattern } from '../../../../../../src/plugins/data/server'; +import { esKuery } from '../../../../../../src/plugins/data/server'; import { JsonObject } from '../../../../../../src/plugins/kibana_utils/common'; import { StatusCheckFilters, @@ -26,7 +26,7 @@ import { UNNAMED_LOCATION } from '../../../common/constants'; import { uptimeAlertWrapper } from './uptime_alert_wrapper'; import { MonitorStatusTranslations } from '../../../common/translations'; import { ESAPICaller } from '../adapters/framework'; -import { getUptimeIndexPattern } from '../requests/get_index_pattern'; +import { getUptimeIndexPattern, IndexPatternTitleAndFields } from '../requests/get_index_pattern'; import { UMServerLibs } from '../lib'; const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; @@ -58,7 +58,7 @@ export const hasFilters = (filters?: StatusCheckFilters) => { }; export const generateFilterDSL = async ( - getIndexPattern: () => Promise, + getIndexPattern: () => Promise, filters: StatusCheckFilters, search: string ): Promise => { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts b/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts index 345d02b990eb7..1d284143a1ab0 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts @@ -6,12 +6,17 @@ import { LegacyAPICaller, LegacyCallAPIOptions } from 'src/core/server'; import { UMElasticsearchQueryFn } from '../adapters'; -import { IndexPatternsFetcher, IIndexPattern } from '../../../../../../src/plugins/data/server'; +import { IndexPatternsFetcher, FieldDescriptor } from '../../../../../../src/plugins/data/server'; -export const getUptimeIndexPattern: UMElasticsearchQueryFn<{}, IIndexPattern | undefined> = async ({ - callES, - dynamicSettings, -}) => { +export interface IndexPatternTitleAndFields { + title: string; + fields: FieldDescriptor[]; +} + +export const getUptimeIndexPattern: UMElasticsearchQueryFn< + {}, + IndexPatternTitleAndFields | undefined +> = async ({ callES, dynamicSettings }) => { const callAsCurrentUser: LegacyAPICaller = async ( endpoint: string, clientParams: Record = {}, @@ -28,7 +33,7 @@ export const getUptimeIndexPattern: UMElasticsearchQueryFn<{}, IIndexPattern | u pattern: dynamicSettings.heartbeatIndices, }); - const indexPattern: IIndexPattern = { + const indexPattern: IndexPatternTitleAndFields = { fields, title: dynamicSettings.heartbeatIndices, }; From 2476451790606520e05c4611301beae4bae74e17 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Tue, 22 Sep 2020 20:12:31 -0400 Subject: [PATCH 26/92] Add `xpack.security.sameSiteCookies` to docker allow list (#78192) --- .../os_packages/docker_generator/resources/bin/kibana-docker | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index b02b7cc16ec4a..884e7e38494cc 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -235,6 +235,7 @@ kibana_vars=( xpack.security.cookieName xpack.security.enabled xpack.security.encryptionKey + xpack.security.sameSiteCookies xpack.security.secureCookies xpack.security.sessionTimeout xpack.security.session.idleTimeout From 9450248ebede86b0cb515bbd1a96046268f4a332 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 22 Sep 2020 20:01:36 -0500 Subject: [PATCH 27/92] Index pattern - refactor constructor (#77791) * index pattern - refactor constructor --- ...uplicateindexpatternerror._constructor_.md | 20 + ...-data-public.duplicateindexpatternerror.md | 18 + ...ata-public.iindexpattern.fieldformatmap.md | 5 +- ...lugin-plugins-data-public.iindexpattern.md | 2 +- ...ta-public.iindexpatternfieldlist.tospec.md | 4 +- ...-data-public.indexpattern._constructor_.md | 5 +- ...s-data-public.indexpattern._fetchfields.md | 15 - ...ta-public.indexpattern.addscriptedfield.md | 6 +- ...plugins-data-public.indexpattern.create.md | 22 - ...data-public.indexpattern.fieldformatmap.md | 2 +- ...plugins-data-public.indexpattern.fields.md | 2 +- ...-data-public.indexpattern.fieldsfetcher.md | 11 - ...ins-data-public.indexpattern.flattenhit.md | 2 +- ...ns-data-public.indexpattern.formatfield.md | 2 +- ...gins-data-public.indexpattern.formathit.md | 5 +- ...blic.indexpattern.getassavedobjectbody.md} | 8 +- ...ublic.indexpattern.getformatterforfield.md | 2 + ...indexpattern.getoriginalsavedobjectbody.md | 22 + ...-public.indexpattern.getsourcefiltering.md | 2 + ...n-plugins-data-public.indexpattern.init.md | 15 - ...s-data-public.indexpattern.initfromspec.md | 22 - ...ins-data-public.indexpattern.iswildcard.md | 15 - ...plugin-plugins-data-public.indexpattern.md | 32 +- ...s-data-public.indexpattern.originalbody.md | 13 - ...-data-public.indexpattern.refreshfields.md | 15 - ...public.indexpattern.removescriptedfield.md | 2 + ...dexpattern.resetoriginalsavedobjectbody.md | 13 + ...gins-data-public.indexpatternattributes.md | 6 - ....indexpatternfield.conflictdescriptions.md | 2 + ...ins-data-public.indexpatternfield.count.md | 2 + ...gins-data-public.indexpatternfield.lang.md | 2 + ...n-plugins-data-public.indexpatternfield.md | 8 +- ...ns-data-public.indexpatternfield.script.md | 2 + ...ins-data-public.indexpatternspec.fields.md | 11 + ...plugins-data-public.indexpatternspec.id.md | 11 + ...ta-public.indexpatternspec.intervalname.md | 11 + ...in-plugins-data-public.indexpatternspec.md | 26 + ...a-public.indexpatternspec.sourcefilters.md | 11 + ...a-public.indexpatternspec.timefieldname.md | 11 + ...gins-data-public.indexpatternspec.title.md | 11 + ...ugins-data-public.indexpatternspec.type.md | 11 + ...s-data-public.indexpatternspec.typemeta.md | 11 + ...ns-data-public.indexpatternspec.version.md | 11 + ...blic.indexpatternsservice._constructor_.md | 20 + ...-public.indexpatternsservice.clearcache.md | 13 + ...data-public.indexpatternsservice.create.md | 25 + ...blic.indexpatternsservice.createandsave.md | 26 + ....indexpatternsservice.createsavedobject.md | 25 + ...data-public.indexpatternsservice.delete.md | 24 + ...tternsservice.ensuredefaultindexpattern.md | 11 + ...ic.indexpatternsservice.fieldarraytomap.md | 13 + ...ns-data-public.indexpatternsservice.get.md | 13 + ...ta-public.indexpatternsservice.getcache.md | 11 + ...-public.indexpatternsservice.getdefault.md | 13 + ...atternsservice.getfieldsforindexpattern.md | 13 + ...dexpatternsservice.getfieldsforwildcard.md | 13 + ...data-public.indexpatternsservice.getids.md | 13 + ...a-public.indexpatternsservice.gettitles.md | 13 + ...lugins-data-public.indexpatternsservice.md | 46 ++ ...blic.indexpatternsservice.refreshfields.md | 13 + ....indexpatternsservice.savedobjecttospec.md | 13 + ...-public.indexpatternsservice.setdefault.md | 13 + ....indexpatternsservice.updatesavedobject.md | 25 + .../kibana-plugin-plugins-data-public.md | 5 +- ...-data-server.indexpattern._constructor_.md | 5 +- ...s-data-server.indexpattern._fetchfields.md | 15 - ...ta-server.indexpattern.addscriptedfield.md | 6 +- ...plugins-data-server.indexpattern.create.md | 22 - ...data-server.indexpattern.fieldformatmap.md | 2 +- ...plugins-data-server.indexpattern.fields.md | 2 +- ...-data-server.indexpattern.fieldsfetcher.md | 11 - ...ins-data-server.indexpattern.flattenhit.md | 2 +- ...ns-data-server.indexpattern.formatfield.md | 2 +- ...gins-data-server.indexpattern.formathit.md | 5 +- ...rver.indexpattern.getassavedobjectbody.md} | 8 +- ...erver.indexpattern.getformatterforfield.md | 2 + ...indexpattern.getoriginalsavedobjectbody.md | 22 + ...-server.indexpattern.getsourcefiltering.md | 2 + ...n-plugins-data-server.indexpattern.init.md | 15 - ...s-data-server.indexpattern.initfromspec.md | 22 - ...ins-data-server.indexpattern.iswildcard.md | 15 - ...plugin-plugins-data-server.indexpattern.md | 32 +- ...s-data-server.indexpattern.originalbody.md | 13 - ...-data-server.indexpattern.refreshfields.md | 15 - ...server.indexpattern.removescriptedfield.md | 2 + ...dexpattern.resetoriginalsavedobjectbody.md | 13 + ...gins-data-server.indexpatternattributes.md | 6 - ...lugins-data-server.indexpatternsservice.md | 19 + ...-data-server.indexpatternsservice.setup.md | 22 + ...-data-server.indexpatternsservice.start.md | 27 + .../kibana-plugin-plugins-data-server.md | 3 +- ...plugin-plugins-data-server.plugin.start.md | 4 +- .../stubbed_saved_object_index_pattern.ts | 4 +- src/plugins/data/common/index.ts | 7 + .../duplicate_index_pattern.ts} | 10 +- .../common/index_patterns/errors/index.ts | 20 + .../index_patterns/fields/field_list.ts | 22 +- .../fields/index_pattern_field.ts | 22 +- .../__snapshots__/index_pattern.test.ts.snap | 325 ++++++------ .../__snapshots__/index_patterns.test.ts.snap | 22 + .../index_patterns/_fields_fetcher.ts | 48 -- .../index_patterns/index_pattern.test.ts | 143 +----- .../index_patterns/index_pattern.ts | 480 ++++++------------ .../index_patterns/index_patterns.test.ts | 66 ++- .../index_patterns/index_patterns.ts | 478 +++++++++++++---- .../data/common/index_patterns/types.ts | 23 +- src/plugins/data/public/index.ts | 5 + src/plugins/data/public/public.api.md | 198 +++++--- src/plugins/data/server/index.ts | 2 + src/plugins/data/server/server.api.md | 101 ++-- .../sidebar/lib/field_calculator.test.ts | 2 +- .../components/table/table.test.tsx | 4 +- .../step_time_field.test.tsx.snap | 14 +- .../step_time_field/step_time_field.test.tsx | 7 +- .../step_time_field/step_time_field.tsx | 8 +- .../create_index_pattern_wizard.test.tsx | 18 +- .../create_index_pattern_wizard.tsx | 67 +-- .../create_edit_field/create_edit_field.tsx | 2 +- .../edit_index_pattern/edit_index_pattern.tsx | 5 +- .../scripted_fields_table.tsx | 2 +- .../source_filters_table.tsx | 2 +- .../edit_index_pattern/tabs/tabs.tsx | 2 +- .../components/field_editor/field_editor.tsx | 4 +- .../public/lib/resolve_saved_objects.ts | 79 +-- .../management/_index_pattern_popularity.js | 2 +- .../plugins/index_patterns/server/plugin.ts | 2 +- .../test_suites/data_plugin/index_patterns.ts | 10 +- .../public/util/indexing_service.js | 29 +- .../use_create_analytics_form.ts | 66 +-- .../components/import_view/import_view.js | 26 +- .../ml/public/application/util/index_utils.ts | 6 +- .../step_create/step_create_form.tsx | 75 ++- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 134 files changed, 1992 insertions(+), 1524 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.duplicateindexpatternerror._constructor_.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.duplicateindexpatternerror.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._fetchfields.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.create.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldsfetcher.md rename docs/development/plugins/data/public/{kibana-plugin-plugins-data-public.indexpattern.prepbody.md => kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md} (76%) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.init.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.initfromspec.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.iswildcard.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.originalbody.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.refreshfields.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.resetoriginalsavedobjectbody.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fields.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.id.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.sourcefilters.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.timefieldname.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.title.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.type.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.typemeta.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.version.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice._constructor_.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.clearcache.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.create.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.createandsave.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.createsavedobject.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.delete.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.ensuredefaultindexpattern.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.get.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getids.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.refreshfields.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.savedobjecttospec.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.setdefault.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.updatesavedobject.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._fetchfields.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.create.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldsfetcher.md rename docs/development/plugins/data/server/{kibana-plugin-plugins-data-server.indexpattern.prepbody.md => kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md} (76%) create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.init.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.initfromspec.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.iswildcard.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.originalbody.md delete mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.refreshfields.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.resetoriginalsavedobjectbody.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md rename src/plugins/data/common/index_patterns/{errors.ts => errors/duplicate_index_pattern.ts} (76%) create mode 100644 src/plugins/data/common/index_patterns/errors/index.ts create mode 100644 src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap delete mode 100644 src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.duplicateindexpatternerror._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.duplicateindexpatternerror._constructor_.md new file mode 100644 index 0000000000000..676f1a2c785f8 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.duplicateindexpatternerror._constructor_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DuplicateIndexPatternError](./kibana-plugin-plugins-data-public.duplicateindexpatternerror.md) > [(constructor)](./kibana-plugin-plugins-data-public.duplicateindexpatternerror._constructor_.md) + +## DuplicateIndexPatternError.(constructor) + +Constructs a new instance of the `DuplicateIndexPatternError` class + +Signature: + +```typescript +constructor(message: string); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| message | string | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.duplicateindexpatternerror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.duplicateindexpatternerror.md new file mode 100644 index 0000000000000..7ed8f97976464 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.duplicateindexpatternerror.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DuplicateIndexPatternError](./kibana-plugin-plugins-data-public.duplicateindexpatternerror.md) + +## DuplicateIndexPatternError class + +Signature: + +```typescript +export declare class DuplicateIndexPatternError extends Error +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(message)](./kibana-plugin-plugins-data-public.duplicateindexpatternerror._constructor_.md) | | Constructs a new instance of the DuplicateIndexPatternError class | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.fieldformatmap.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.fieldformatmap.md index 2c131c6da9937..60ac95bc21af2 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.fieldformatmap.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.fieldformatmap.md @@ -7,8 +7,5 @@ Signature: ```typescript -fieldFormatMap?: Record; +fieldFormatMap?: Record | undefined>; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md index b631d4dd7800a..ba77e659f0834 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md @@ -14,7 +14,7 @@ export interface IIndexPattern | Property | Type | Description | | --- | --- | --- | -| [fieldFormatMap](./kibana-plugin-plugins-data-public.iindexpattern.fieldformatmap.md) | Record<string, {
id: string;
params: unknown;
}> | | +| [fieldFormatMap](./kibana-plugin-plugins-data-public.iindexpattern.fieldformatmap.md) | Record<string, SerializedFieldFormat<unknown> | undefined> | | | [fields](./kibana-plugin-plugins-data-public.iindexpattern.fields.md) | IFieldType[] | | | [getFormatterForField](./kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md) | (field: IndexPatternField | IndexPatternField['spec'] | IFieldType) => FieldFormat | | | [id](./kibana-plugin-plugins-data-public.iindexpattern.id.md) | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md index fd20f2944c5be..0fe62f575a927 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md @@ -9,7 +9,7 @@ ```typescript toSpec(options?: { getFormatterForField?: IndexPattern['getFormatterForField']; - }): FieldSpec[]; + }): IndexPatternFieldMap; ``` ## Parameters @@ -20,5 +20,5 @@ toSpec(options?: { Returns: -`FieldSpec[]` +`IndexPatternFieldMap` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md index a5bb15c963978..4baf98038f89a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md @@ -9,13 +9,12 @@ Constructs a new instance of the `IndexPattern` class Signature: ```typescript -constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps); +constructor({ spec, savedObjectsClient, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| id | string | undefined | | -| { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, } | IndexPatternDeps | | +| { spec, savedObjectsClient, fieldFormats, shortDotsEnable, metaFields, } | IndexPatternDeps | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._fetchfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._fetchfields.md deleted file mode 100644 index 8fff8baa71139..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._fetchfields.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [\_fetchFields](./kibana-plugin-plugins-data-public.indexpattern._fetchfields.md) - -## IndexPattern.\_fetchFields() method - -Signature: - -```typescript -_fetchFields(): Promise; -``` -Returns: - -`Promise` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md index 4bbbd83c65e10..cc3468531fffa 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md @@ -4,10 +4,12 @@ ## IndexPattern.addScriptedField() method +Add scripted field to field list + Signature: ```typescript -addScriptedField(name: string, script: string, fieldType: string | undefined, lang: string): Promise; +addScriptedField(name: string, script: string, fieldType?: string, lang?: string): Promise; ``` ## Parameters @@ -16,7 +18,7 @@ addScriptedField(name: string, script: string, fieldType: string | undefined, la | --- | --- | --- | | name | string | | | script | string | | -| fieldType | string | undefined | | +| fieldType | string | | | lang | string | | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.create.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.create.md deleted file mode 100644 index 5c122b835f59d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.create.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [create](./kibana-plugin-plugins-data-public.indexpattern.create.md) - -## IndexPattern.create() method - -Signature: - -```typescript -create(allowOverride?: boolean): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| allowOverride | boolean | | - -Returns: - -`Promise` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md index b89b244d9826c..904d52fcd5751 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md @@ -7,5 +7,5 @@ Signature: ```typescript -fieldFormatMap: any; +fieldFormatMap: Record; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fields.md index d4dca48c7cd7b..76bc41238526e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fields.md @@ -8,6 +8,6 @@ ```typescript fields: IIndexPatternFieldList & { - toSpec: () => FieldSpec[]; + toSpec: () => IndexPatternFieldMap; }; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldsfetcher.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldsfetcher.md deleted file mode 100644 index 4d44b386a1db1..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldsfetcher.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [fieldsFetcher](./kibana-plugin-plugins-data-public.indexpattern.fieldsfetcher.md) - -## IndexPattern.fieldsFetcher property - -Signature: - -```typescript -fieldsFetcher: any; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.flattenhit.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.flattenhit.md index db28d95197bb3..049c3e5e990f7 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.flattenhit.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.flattenhit.md @@ -7,5 +7,5 @@ Signature: ```typescript -flattenHit: any; +flattenHit: (hit: Record, deep?: boolean) => Record; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.formatfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.formatfield.md index 5a475d6161ac3..aadaddca6cc85 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.formatfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.formatfield.md @@ -7,5 +7,5 @@ Signature: ```typescript -formatField: any; +formatField: FormatFieldFn; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.formathit.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.formathit.md index ac515d374a93f..2be76bf1c1e05 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.formathit.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.formathit.md @@ -7,5 +7,8 @@ Signature: ```typescript -formatHit: any; +formatHit: { + (hit: Record, type?: string): any; + formatField: FormatFieldFn; + }; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.prepbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md similarity index 76% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.prepbody.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md index 1d77b2a55860e..2c5f30e4889ea 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.prepbody.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md @@ -1,13 +1,15 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [prepBody](./kibana-plugin-plugins-data-public.indexpattern.prepbody.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [getAsSavedObjectBody](./kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md) -## IndexPattern.prepBody() method +## IndexPattern.getAsSavedObjectBody() method + +Returns index pattern as saved object body for saving Signature: ```typescript -prepBody(): { +getAsSavedObjectBody(): { title: string; timeFieldName: string | undefined; intervalName: string | undefined; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md index 180b2d8a7b03a..ba31d60b56892 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md @@ -4,6 +4,8 @@ ## IndexPattern.getFormatterForField() method +Provide a field, get its formatter + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md new file mode 100644 index 0000000000000..349da63c13ca7 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md) + +## IndexPattern.getOriginalSavedObjectBody property + +Get last saved saved object fields + +Signature: + +```typescript +getOriginalSavedObjectBody: () => { + title?: string | undefined; + timeFieldName?: string | undefined; + intervalName?: string | undefined; + fields?: string | undefined; + sourceFilters?: string | undefined; + fieldFormatMap?: string | undefined; + typeMeta?: string | undefined; + type?: string | undefined; + }; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md index 121d32c7c40c8..4ce0144b73882 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md @@ -4,6 +4,8 @@ ## IndexPattern.getSourceFiltering() method +Get the source filtering configuration for that index. + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.init.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.init.md deleted file mode 100644 index 595992dc82b74..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.init.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [init](./kibana-plugin-plugins-data-public.indexpattern.init.md) - -## IndexPattern.init() method - -Signature: - -```typescript -init(): Promise; -``` -Returns: - -`Promise` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.initfromspec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.initfromspec.md deleted file mode 100644 index 764dd11638221..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.initfromspec.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [initFromSpec](./kibana-plugin-plugins-data-public.indexpattern.initfromspec.md) - -## IndexPattern.initFromSpec() method - -Signature: - -```typescript -initFromSpec(spec: IndexPatternSpec): this; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| spec | IndexPatternSpec | | - -Returns: - -`this` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.iswildcard.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.iswildcard.md deleted file mode 100644 index e5ea55ef1dd48..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.iswildcard.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [isWildcard](./kibana-plugin-plugins-data-public.indexpattern.iswildcard.md) - -## IndexPattern.isWildcard() method - -Signature: - -```typescript -isWildcard(): boolean; -``` -Returns: - -`boolean` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index 87ce1e258712a..2ff575bc4fc22 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -14,22 +14,22 @@ export declare class IndexPattern implements IIndexPattern | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(id, { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, })](./kibana-plugin-plugins-data-public.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | +| [(constructor)({ spec, savedObjectsClient, fieldFormats, shortDotsEnable, metaFields, })](./kibana-plugin-plugins-data-public.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | ## Properties | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md) | | any | | -| [fields](./kibana-plugin-plugins-data-public.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => FieldSpec[];
} | | -| [fieldsFetcher](./kibana-plugin-plugins-data-public.indexpattern.fieldsfetcher.md) | | any | | -| [flattenHit](./kibana-plugin-plugins-data-public.indexpattern.flattenhit.md) | | any | | -| [formatField](./kibana-plugin-plugins-data-public.indexpattern.formatfield.md) | | any | | -| [formatHit](./kibana-plugin-plugins-data-public.indexpattern.formathit.md) | | any | | +| [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md) | | Record<string, any> | | +| [fields](./kibana-plugin-plugins-data-public.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => IndexPatternFieldMap;
} | | +| [flattenHit](./kibana-plugin-plugins-data-public.indexpattern.flattenhit.md) | | (hit: Record<string, any>, deep?: boolean) => Record<string, any> | | +| [formatField](./kibana-plugin-plugins-data-public.indexpattern.formatfield.md) | | FormatFieldFn | | +| [formatHit](./kibana-plugin-plugins-data-public.indexpattern.formathit.md) | | {
(hit: Record<string, any>, type?: string): any;
formatField: FormatFieldFn;
} | | +| [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md) | | () => {
title?: string | undefined;
timeFieldName?: string | undefined;
intervalName?: string | undefined;
fields?: string | undefined;
sourceFilters?: string | undefined;
fieldFormatMap?: string | undefined;
typeMeta?: string | undefined;
type?: string | undefined;
} | Get last saved saved object fields | | [id](./kibana-plugin-plugins-data-public.indexpattern.id.md) | | string | | | [intervalName](./kibana-plugin-plugins-data-public.indexpattern.intervalname.md) | | string | undefined | | | [metaFields](./kibana-plugin-plugins-data-public.indexpattern.metafields.md) | | string[] | | -| [originalBody](./kibana-plugin-plugins-data-public.indexpattern.originalbody.md) | | {
[key: string]: any;
} | | +| [resetOriginalSavedObjectBody](./kibana-plugin-plugins-data-public.indexpattern.resetoriginalsavedobjectbody.md) | | () => void | Reset last saved saved object fields. used after saving | | [sourceFilters](./kibana-plugin-plugins-data-public.indexpattern.sourcefilters.md) | | SourceFilter[] | | | [timeFieldName](./kibana-plugin-plugins-data-public.indexpattern.timefieldname.md) | | string | undefined | | | [title](./kibana-plugin-plugins-data-public.indexpattern.title.md) | | string | | @@ -41,26 +41,20 @@ export declare class IndexPattern implements IIndexPattern | Method | Modifiers | Description | | --- | --- | --- | -| [\_fetchFields()](./kibana-plugin-plugins-data-public.indexpattern._fetchfields.md) | | | -| [addScriptedField(name, script, fieldType, lang)](./kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md) | | | -| [create(allowOverride)](./kibana-plugin-plugins-data-public.indexpattern.create.md) | | | +| [addScriptedField(name, script, fieldType, lang)](./kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md) | | Add scripted field to field list | | [getAggregationRestrictions()](./kibana-plugin-plugins-data-public.indexpattern.getaggregationrestrictions.md) | | | +| [getAsSavedObjectBody()](./kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md) | | Returns index pattern as saved object body for saving | | [getComputedFields()](./kibana-plugin-plugins-data-public.indexpattern.getcomputedfields.md) | | | | [getFieldByName(name)](./kibana-plugin-plugins-data-public.indexpattern.getfieldbyname.md) | | | -| [getFormatterForField(field)](./kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md) | | | +| [getFormatterForField(field)](./kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md) | | Provide a field, get its formatter | | [getNonScriptedFields()](./kibana-plugin-plugins-data-public.indexpattern.getnonscriptedfields.md) | | | | [getScriptedFields()](./kibana-plugin-plugins-data-public.indexpattern.getscriptedfields.md) | | | -| [getSourceFiltering()](./kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md) | | | +| [getSourceFiltering()](./kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md) | | Get the source filtering configuration for that index. | | [getTimeField()](./kibana-plugin-plugins-data-public.indexpattern.gettimefield.md) | | | -| [init()](./kibana-plugin-plugins-data-public.indexpattern.init.md) | | | -| [initFromSpec(spec)](./kibana-plugin-plugins-data-public.indexpattern.initfromspec.md) | | | | [isTimeBased()](./kibana-plugin-plugins-data-public.indexpattern.istimebased.md) | | | | [isTimeBasedWildcard()](./kibana-plugin-plugins-data-public.indexpattern.istimebasedwildcard.md) | | | | [isTimeNanosBased()](./kibana-plugin-plugins-data-public.indexpattern.istimenanosbased.md) | | | -| [isWildcard()](./kibana-plugin-plugins-data-public.indexpattern.iswildcard.md) | | | | [popularizeField(fieldName, unit)](./kibana-plugin-plugins-data-public.indexpattern.popularizefield.md) | | | -| [prepBody()](./kibana-plugin-plugins-data-public.indexpattern.prepbody.md) | | | -| [refreshFields()](./kibana-plugin-plugins-data-public.indexpattern.refreshfields.md) | | | -| [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md) | | | +| [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md) | | Remove scripted field from field list | | [toSpec()](./kibana-plugin-plugins-data-public.indexpattern.tospec.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.originalbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.originalbody.md deleted file mode 100644 index 4bc3c76afbae9..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.originalbody.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [originalBody](./kibana-plugin-plugins-data-public.indexpattern.originalbody.md) - -## IndexPattern.originalBody property - -Signature: - -```typescript -originalBody: { - [key: string]: any; - }; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.refreshfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.refreshfields.md deleted file mode 100644 index 271d0c45a4244..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.refreshfields.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [refreshFields](./kibana-plugin-plugins-data-public.indexpattern.refreshfields.md) - -## IndexPattern.refreshFields() method - -Signature: - -```typescript -refreshFields(): Promise; -``` -Returns: - -`Promise` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md index e902d9c42b082..aaaebdaccca5d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md @@ -4,6 +4,8 @@ ## IndexPattern.removeScriptedField() method +Remove scripted field from field list + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.resetoriginalsavedobjectbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.resetoriginalsavedobjectbody.md new file mode 100644 index 0000000000000..6bbc13d8fd410 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.resetoriginalsavedobjectbody.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [resetOriginalSavedObjectBody](./kibana-plugin-plugins-data-public.indexpattern.resetoriginalsavedobjectbody.md) + +## IndexPattern.resetOriginalSavedObjectBody property + +Reset last saved saved object fields. used after saving + +Signature: + +```typescript +resetOriginalSavedObjectBody: () => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md index eff2349f053ff..77a8ebb0b2d3f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md @@ -4,12 +4,6 @@ ## IndexPatternAttributes interface -> Warning: This API is now obsolete. -> -> - -Use data plugin interface instead - Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md index 6d62053726197..9b226266f0b5a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md @@ -4,6 +4,8 @@ ## IndexPatternField.conflictDescriptions property +Description of field type conflicts across different indices in the same index pattern + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.count.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.count.md index 84c0a75fd206d..1b8e13a38c6d9 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.count.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.count.md @@ -4,6 +4,8 @@ ## IndexPatternField.count property +Count is used for field popularity + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.lang.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.lang.md index 0a8446d40e5ec..b81218eb08886 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.lang.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.lang.md @@ -4,6 +4,8 @@ ## IndexPatternField.lang property +Script field language + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md index 215188ffa2607..4f49a9a8fc3ab 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md @@ -21,15 +21,15 @@ export declare class IndexPatternField implements IFieldType | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [aggregatable](./kibana-plugin-plugins-data-public.indexpatternfield.aggregatable.md) | | boolean | | -| [conflictDescriptions](./kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md) | | Record<string, string[]> | undefined | | -| [count](./kibana-plugin-plugins-data-public.indexpatternfield.count.md) | | number | | +| [conflictDescriptions](./kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md) | | Record<string, string[]> | undefined | Description of field type conflicts across different indices in the same index pattern | +| [count](./kibana-plugin-plugins-data-public.indexpatternfield.count.md) | | number | Count is used for field popularity | | [displayName](./kibana-plugin-plugins-data-public.indexpatternfield.displayname.md) | | string | | | [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) | | string[] | undefined | | | [filterable](./kibana-plugin-plugins-data-public.indexpatternfield.filterable.md) | | boolean | | -| [lang](./kibana-plugin-plugins-data-public.indexpatternfield.lang.md) | | string | undefined | | +| [lang](./kibana-plugin-plugins-data-public.indexpatternfield.lang.md) | | string | undefined | Script field language | | [name](./kibana-plugin-plugins-data-public.indexpatternfield.name.md) | | string | | | [readFromDocValues](./kibana-plugin-plugins-data-public.indexpatternfield.readfromdocvalues.md) | | boolean | | -| [script](./kibana-plugin-plugins-data-public.indexpatternfield.script.md) | | string | undefined | | +| [script](./kibana-plugin-plugins-data-public.indexpatternfield.script.md) | | string | undefined | Script field code | | [scripted](./kibana-plugin-plugins-data-public.indexpatternfield.scripted.md) | | boolean | | | [searchable](./kibana-plugin-plugins-data-public.indexpatternfield.searchable.md) | | boolean | | | [sortable](./kibana-plugin-plugins-data-public.indexpatternfield.sortable.md) | | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.script.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.script.md index 27f9c797c92f2..7501e191d9363 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.script.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.script.md @@ -4,6 +4,8 @@ ## IndexPatternField.script property +Script field code + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fields.md new file mode 100644 index 0000000000000..386e080dbe6c2 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fields.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [fields](./kibana-plugin-plugins-data-public.indexpatternspec.fields.md) + +## IndexPatternSpec.fields property + +Signature: + +```typescript +fields?: IndexPatternFieldMap; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.id.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.id.md new file mode 100644 index 0000000000000..55eadbf36c660 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [id](./kibana-plugin-plugins-data-public.indexpatternspec.id.md) + +## IndexPatternSpec.id property + +Signature: + +```typescript +id?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md new file mode 100644 index 0000000000000..98748661256da --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [intervalName](./kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md) + +## IndexPatternSpec.intervalName property + +Signature: + +```typescript +intervalName?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md new file mode 100644 index 0000000000000..74c4df126e1bf --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) + +## IndexPatternSpec interface + +Signature: + +```typescript +export interface IndexPatternSpec +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [fields](./kibana-plugin-plugins-data-public.indexpatternspec.fields.md) | IndexPatternFieldMap | | +| [id](./kibana-plugin-plugins-data-public.indexpatternspec.id.md) | string | | +| [intervalName](./kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md) | string | | +| [sourceFilters](./kibana-plugin-plugins-data-public.indexpatternspec.sourcefilters.md) | SourceFilter[] | | +| [timeFieldName](./kibana-plugin-plugins-data-public.indexpatternspec.timefieldname.md) | string | | +| [title](./kibana-plugin-plugins-data-public.indexpatternspec.title.md) | string | | +| [type](./kibana-plugin-plugins-data-public.indexpatternspec.type.md) | string | | +| [typeMeta](./kibana-plugin-plugins-data-public.indexpatternspec.typemeta.md) | TypeMeta | | +| [version](./kibana-plugin-plugins-data-public.indexpatternspec.version.md) | string | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.sourcefilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.sourcefilters.md new file mode 100644 index 0000000000000..cda5285730135 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.sourcefilters.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [sourceFilters](./kibana-plugin-plugins-data-public.indexpatternspec.sourcefilters.md) + +## IndexPatternSpec.sourceFilters property + +Signature: + +```typescript +sourceFilters?: SourceFilter[]; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.timefieldname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.timefieldname.md new file mode 100644 index 0000000000000..a527e3ac0658b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.timefieldname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [timeFieldName](./kibana-plugin-plugins-data-public.indexpatternspec.timefieldname.md) + +## IndexPatternSpec.timeFieldName property + +Signature: + +```typescript +timeFieldName?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.title.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.title.md new file mode 100644 index 0000000000000..4cc6d3c2524a7 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.title.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [title](./kibana-plugin-plugins-data-public.indexpatternspec.title.md) + +## IndexPatternSpec.title property + +Signature: + +```typescript +title?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.type.md new file mode 100644 index 0000000000000..d1c49be1b706f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [type](./kibana-plugin-plugins-data-public.indexpatternspec.type.md) + +## IndexPatternSpec.type property + +Signature: + +```typescript +type?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.typemeta.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.typemeta.md new file mode 100644 index 0000000000000..9303047e905d3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.typemeta.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [typeMeta](./kibana-plugin-plugins-data-public.indexpatternspec.typemeta.md) + +## IndexPatternSpec.typeMeta property + +Signature: + +```typescript +typeMeta?: TypeMeta; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.version.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.version.md new file mode 100644 index 0000000000000..43f7cf0226fb0 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.version.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [version](./kibana-plugin-plugins-data-public.indexpatternspec.version.md) + +## IndexPatternSpec.version property + +Signature: + +```typescript +version?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice._constructor_.md new file mode 100644 index 0000000000000..ab397efb1fe0e --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice._constructor_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [(constructor)](./kibana-plugin-plugins-data-public.indexpatternsservice._constructor_.md) + +## IndexPatternsService.(constructor) + +Constructs a new instance of the `IndexPatternsService` class + +Signature: + +```typescript +constructor({ uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, }: IndexPatternsServiceDeps); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, } | IndexPatternsServiceDeps | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.clearcache.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.clearcache.md new file mode 100644 index 0000000000000..b371218325086 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.clearcache.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [clearCache](./kibana-plugin-plugins-data-public.indexpatternsservice.clearcache.md) + +## IndexPatternsService.clearCache property + +Clear index pattern list cache + +Signature: + +```typescript +clearCache: (id?: string | undefined) => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.create.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.create.md new file mode 100644 index 0000000000000..d7152ba617cc6 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.create.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [create](./kibana-plugin-plugins-data-public.indexpatternsservice.create.md) + +## IndexPatternsService.create() method + +Create a new index pattern instance + +Signature: + +```typescript +create(spec: IndexPatternSpec, skipFetchFields?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| spec | IndexPatternSpec | | +| skipFetchFields | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.createandsave.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.createandsave.md new file mode 100644 index 0000000000000..eebfbb506fb77 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.createandsave.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [createAndSave](./kibana-plugin-plugins-data-public.indexpatternsservice.createandsave.md) + +## IndexPatternsService.createAndSave() method + +Create a new index pattern and save it right away + +Signature: + +```typescript +createAndSave(spec: IndexPatternSpec, override?: boolean, skipFetchFields?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| spec | IndexPatternSpec | | +| override | boolean | | +| skipFetchFields | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.createsavedobject.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.createsavedobject.md new file mode 100644 index 0000000000000..8efb33c423b01 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.createsavedobject.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [createSavedObject](./kibana-plugin-plugins-data-public.indexpatternsservice.createsavedobject.md) + +## IndexPatternsService.createSavedObject() method + +Save a new index pattern + +Signature: + +```typescript +createSavedObject(indexPattern: IndexPattern, override?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPattern | IndexPattern | | +| override | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.delete.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.delete.md new file mode 100644 index 0000000000000..aba31ab2c0d29 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.delete.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [delete](./kibana-plugin-plugins-data-public.indexpatternsservice.delete.md) + +## IndexPatternsService.delete() method + +Deletes an index pattern from .kibana index + +Signature: + +```typescript +delete(indexPatternId: string): Promise<{}>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPatternId | string | | + +Returns: + +`Promise<{}>` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.ensuredefaultindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.ensuredefaultindexpattern.md new file mode 100644 index 0000000000000..3b6a8c7e4a04f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.ensuredefaultindexpattern.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [ensureDefaultIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.ensuredefaultindexpattern.md) + +## IndexPatternsService.ensureDefaultIndexPattern property + +Signature: + +```typescript +ensureDefaultIndexPattern: EnsureDefaultIndexPattern; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md new file mode 100644 index 0000000000000..ed365fe03f980 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [fieldArrayToMap](./kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md) + +## IndexPatternsService.fieldArrayToMap property + +Converts field array to map + +Signature: + +```typescript +fieldArrayToMap: (fields: FieldSpec[]) => Record; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.get.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.get.md new file mode 100644 index 0000000000000..4aad6df6b413b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [get](./kibana-plugin-plugins-data-public.indexpatternsservice.get.md) + +## IndexPatternsService.get property + +Get an index pattern by id. Cache optimized + +Signature: + +```typescript +get: (id: string) => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md new file mode 100644 index 0000000000000..ad2a167bd8c74 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [getCache](./kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md) + +## IndexPatternsService.getCache property + +Signature: + +```typescript +getCache: () => Promise[] | null | undefined>; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md new file mode 100644 index 0000000000000..01d4efeffe921 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [getDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md) + +## IndexPatternsService.getDefault property + +Get default index pattern + +Signature: + +```typescript +getDefault: () => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md new file mode 100644 index 0000000000000..c06c3c6f68492 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [getFieldsForIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md) + +## IndexPatternsService.getFieldsForIndexPattern property + +Get field list by providing an index patttern (or spec) + +Signature: + +```typescript +getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions) => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md new file mode 100644 index 0000000000000..aec84866b9e58 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [getFieldsForWildcard](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md) + +## IndexPatternsService.getFieldsForWildcard property + +Get field list by providing { pattern } + +Signature: + +```typescript +getFieldsForWildcard: (options?: GetFieldsOptions) => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getids.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getids.md new file mode 100644 index 0000000000000..a012e0dc9d9c5 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getids.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [getIds](./kibana-plugin-plugins-data-public.indexpatternsservice.getids.md) + +## IndexPatternsService.getIds property + +Get list of index pattern ids + +Signature: + +```typescript +getIds: (refresh?: boolean) => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md new file mode 100644 index 0000000000000..04cc294a79dfc --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [getTitles](./kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md) + +## IndexPatternsService.getTitles property + +Get list of index pattern titles + +Signature: + +```typescript +getTitles: (refresh?: boolean) => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md new file mode 100644 index 0000000000000..0022bff34a8e7 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md @@ -0,0 +1,46 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) + +## IndexPatternsService class + +Signature: + +```typescript +export declare class IndexPatternsService +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)({ uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, })](./kibana-plugin-plugins-data-public.indexpatternsservice._constructor_.md) | | Constructs a new instance of the IndexPatternsService class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [clearCache](./kibana-plugin-plugins-data-public.indexpatternsservice.clearcache.md) | | (id?: string | undefined) => void | Clear index pattern list cache | +| [ensureDefaultIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.ensuredefaultindexpattern.md) | | EnsureDefaultIndexPattern | | +| [fieldArrayToMap](./kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md) | | (fields: FieldSpec[]) => Record<string, FieldSpec> | Converts field array to map | +| [get](./kibana-plugin-plugins-data-public.indexpatternsservice.get.md) | | (id: string) => Promise<IndexPattern> | Get an index pattern by id. Cache optimized | +| [getCache](./kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<IndexPatternSavedObjectAttrs>[] | null | undefined> | | +| [getDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md) | | () => Promise<IndexPattern | null> | Get default index pattern | +| [getFieldsForIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions) => Promise<any> | Get field list by providing an index patttern (or spec) | +| [getFieldsForWildcard](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md) | | (options?: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | +| [getIds](./kibana-plugin-plugins-data-public.indexpatternsservice.getids.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern ids | +| [getTitles](./kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern titles | +| [refreshFields](./kibana-plugin-plugins-data-public.indexpatternsservice.refreshfields.md) | | (indexPattern: IndexPattern) => Promise<void> | Refresh field list for a given index pattern | +| [savedObjectToSpec](./kibana-plugin-plugins-data-public.indexpatternsservice.savedobjecttospec.md) | | (savedObject: SavedObject<IndexPatternAttributes>) => IndexPatternSpec | Converts index pattern saved object to index pattern spec | +| [setDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.setdefault.md) | | (id: string, force?: boolean) => Promise<void> | Optionally set default index pattern, unless force = true | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [create(spec, skipFetchFields)](./kibana-plugin-plugins-data-public.indexpatternsservice.create.md) | | Create a new index pattern instance | +| [createAndSave(spec, override, skipFetchFields)](./kibana-plugin-plugins-data-public.indexpatternsservice.createandsave.md) | | Create a new index pattern and save it right away | +| [createSavedObject(indexPattern, override)](./kibana-plugin-plugins-data-public.indexpatternsservice.createsavedobject.md) | | Save a new index pattern | +| [delete(indexPatternId)](./kibana-plugin-plugins-data-public.indexpatternsservice.delete.md) | | Deletes an index pattern from .kibana index | +| [updateSavedObject(indexPattern, saveAttempts)](./kibana-plugin-plugins-data-public.indexpatternsservice.updatesavedobject.md) | | Save existing index pattern. Will attempt to merge differences if there are conflicts | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.refreshfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.refreshfields.md new file mode 100644 index 0000000000000..b7c47efbb445a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.refreshfields.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [refreshFields](./kibana-plugin-plugins-data-public.indexpatternsservice.refreshfields.md) + +## IndexPatternsService.refreshFields property + +Refresh field list for a given index pattern + +Signature: + +```typescript +refreshFields: (indexPattern: IndexPattern) => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.savedobjecttospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.savedobjecttospec.md new file mode 100644 index 0000000000000..7bd40c9cafd42 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.savedobjecttospec.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [savedObjectToSpec](./kibana-plugin-plugins-data-public.indexpatternsservice.savedobjecttospec.md) + +## IndexPatternsService.savedObjectToSpec property + +Converts index pattern saved object to index pattern spec + +Signature: + +```typescript +savedObjectToSpec: (savedObject: SavedObject) => IndexPatternSpec; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.setdefault.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.setdefault.md new file mode 100644 index 0000000000000..2bf8eaa03d1ae --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.setdefault.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [setDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.setdefault.md) + +## IndexPatternsService.setDefault property + +Optionally set default index pattern, unless force = true + +Signature: + +```typescript +setDefault: (id: string, force?: boolean) => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.updatesavedobject.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.updatesavedobject.md new file mode 100644 index 0000000000000..3973f5d4c3e7b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.updatesavedobject.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [updateSavedObject](./kibana-plugin-plugins-data-public.indexpatternsservice.updatesavedobject.md) + +## IndexPatternsService.updateSavedObject() method + +Save existing index pattern. Will attempt to merge differences if there are conflicts + +Signature: + +```typescript +updateSavedObject(indexPattern: IndexPattern, saveAttempts?: number): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPattern | IndexPattern | | +| saveAttempts | number | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 71f66a1b46d85..506f141984052 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -11,11 +11,13 @@ | [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) | | | [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) | | | [AggParamType](./kibana-plugin-plugins-data-public.aggparamtype.md) | | +| [DuplicateIndexPatternError](./kibana-plugin-plugins-data-public.duplicateindexpatternerror.md) | | | [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) | | | [FilterManager](./kibana-plugin-plugins-data-public.filtermanager.md) | | | [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) | | | [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) | | | [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) | | +| [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-public.optionedparamtype.md) | | | [Plugin](./kibana-plugin-plugins-data-public.plugin.md) | | | [RequestTimeoutError](./kibana-plugin-plugins-data-public.requesttimeouterror.md) | Class used to signify that a request timed out. Useful for applications to conditionally handle this type of error differently than other errors. | @@ -66,7 +68,8 @@ | [IIndexPatternFieldList](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.md) | | | [IKibanaSearchRequest](./kibana-plugin-plugins-data-public.ikibanasearchrequest.md) | | | [IKibanaSearchResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.md) | | -| [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) | Use data plugin interface instead | +| [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) | | +| [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) | | | [IndexPatternTypeMeta](./kibana-plugin-plugins-data-public.indexpatterntypemeta.md) | | | [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) | | | [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) | The setup contract exposed by the Search plugin exposes the search strategy extension point. | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._constructor_.md index d1d31f1f0428e..f7f8e51c4b632 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._constructor_.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._constructor_.md @@ -9,13 +9,12 @@ Constructs a new instance of the `IndexPattern` class Signature: ```typescript -constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps); +constructor({ spec, savedObjectsClient, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| id | string | undefined | | -| { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, } | IndexPatternDeps | | +| { spec, savedObjectsClient, fieldFormats, shortDotsEnable, metaFields, } | IndexPatternDeps | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._fetchfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._fetchfields.md deleted file mode 100644 index d1dabe59d4c45..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._fetchfields.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [\_fetchFields](./kibana-plugin-plugins-data-server.indexpattern._fetchfields.md) - -## IndexPattern.\_fetchFields() method - -Signature: - -```typescript -_fetchFields(): Promise; -``` -Returns: - -`Promise` - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md index 320698b05e323..6d206e88b5b56 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md @@ -4,10 +4,12 @@ ## IndexPattern.addScriptedField() method +Add scripted field to field list + Signature: ```typescript -addScriptedField(name: string, script: string, fieldType: string | undefined, lang: string): Promise; +addScriptedField(name: string, script: string, fieldType?: string, lang?: string): Promise; ``` ## Parameters @@ -16,7 +18,7 @@ addScriptedField(name: string, script: string, fieldType: string | undefined, la | --- | --- | --- | | name | string | | | script | string | | -| fieldType | string | undefined | | +| fieldType | string | | | lang | string | | Returns: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.create.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.create.md deleted file mode 100644 index 82367e79480f1..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.create.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [create](./kibana-plugin-plugins-data-server.indexpattern.create.md) - -## IndexPattern.create() method - -Signature: - -```typescript -create(allowOverride?: boolean): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| allowOverride | boolean | | - -Returns: - -`Promise` - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md index 77e5d112a3db2..2f686bd313d58 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md @@ -7,5 +7,5 @@ Signature: ```typescript -fieldFormatMap: any; +fieldFormatMap: Record; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fields.md index 17a63be92fedf..5b22014486c02 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fields.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fields.md @@ -8,6 +8,6 @@ ```typescript fields: IIndexPatternFieldList & { - toSpec: () => FieldSpec[]; + toSpec: () => IndexPatternFieldMap; }; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldsfetcher.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldsfetcher.md deleted file mode 100644 index 31683934e2252..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldsfetcher.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [fieldsFetcher](./kibana-plugin-plugins-data-server.indexpattern.fieldsfetcher.md) - -## IndexPattern.fieldsFetcher property - -Signature: - -```typescript -fieldsFetcher: any; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.flattenhit.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.flattenhit.md index 3f4851daaf488..33c6dedc6dcd8 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.flattenhit.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.flattenhit.md @@ -7,5 +7,5 @@ Signature: ```typescript -flattenHit: any; +flattenHit: (hit: Record, deep?: boolean) => Record; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formatfield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formatfield.md index 9019904cf2b65..07db8a0805b07 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formatfield.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formatfield.md @@ -7,5 +7,5 @@ Signature: ```typescript -formatField: any; +formatField: FormatFieldFn; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formathit.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formathit.md index 0bfd7466fb3a5..75f282a8991fc 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formathit.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formathit.md @@ -7,5 +7,8 @@ Signature: ```typescript -formatHit: any; +formatHit: { + (hit: Record, type?: string): any; + formatField: FormatFieldFn; + }; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.prepbody.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md similarity index 76% rename from docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.prepbody.md rename to docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md index 0de4418da46f8..f1bdb2f729414 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.prepbody.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md @@ -1,13 +1,15 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [prepBody](./kibana-plugin-plugins-data-server.indexpattern.prepbody.md) +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getAsSavedObjectBody](./kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md) -## IndexPattern.prepBody() method +## IndexPattern.getAsSavedObjectBody() method + +Returns index pattern as saved object body for saving Signature: ```typescript -prepBody(): { +getAsSavedObjectBody(): { title: string; timeFieldName: string | undefined; intervalName: string | undefined; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md index 3218187696918..7dc2756009f4e 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md @@ -4,6 +4,8 @@ ## IndexPattern.getFormatterForField() method +Provide a field, get its formatter + Signature: ```typescript diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md new file mode 100644 index 0000000000000..324f9d0152ab5 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md) + +## IndexPattern.getOriginalSavedObjectBody property + +Get last saved saved object fields + +Signature: + +```typescript +getOriginalSavedObjectBody: () => { + title?: string | undefined; + timeFieldName?: string | undefined; + intervalName?: string | undefined; + fields?: string | undefined; + sourceFilters?: string | undefined; + fieldFormatMap?: string | undefined; + typeMeta?: string | undefined; + type?: string | undefined; + }; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md index f463dcd29a2e4..240f9b4fb0aa2 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md @@ -4,6 +4,8 @@ ## IndexPattern.getSourceFiltering() method +Get the source filtering configuration for that index. + Signature: ```typescript diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.init.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.init.md deleted file mode 100644 index bc17ff00cc9cf..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.init.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [init](./kibana-plugin-plugins-data-server.indexpattern.init.md) - -## IndexPattern.init() method - -Signature: - -```typescript -init(): Promise; -``` -Returns: - -`Promise` - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.initfromspec.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.initfromspec.md deleted file mode 100644 index 6fbf621254ff3..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.initfromspec.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [initFromSpec](./kibana-plugin-plugins-data-server.indexpattern.initfromspec.md) - -## IndexPattern.initFromSpec() method - -Signature: - -```typescript -initFromSpec(spec: IndexPatternSpec): this; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| spec | IndexPatternSpec | | - -Returns: - -`this` - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.iswildcard.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.iswildcard.md deleted file mode 100644 index e0adf71b45efa..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.iswildcard.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [isWildcard](./kibana-plugin-plugins-data-server.indexpattern.iswildcard.md) - -## IndexPattern.isWildcard() method - -Signature: - -```typescript -isWildcard(): boolean; -``` -Returns: - -`boolean` - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md index 27d7dd0315753..d877854444a09 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md @@ -14,22 +14,22 @@ export declare class IndexPattern implements IIndexPattern | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(id, { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, })](./kibana-plugin-plugins-data-server.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | +| [(constructor)({ spec, savedObjectsClient, fieldFormats, shortDotsEnable, metaFields, })](./kibana-plugin-plugins-data-server.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | ## Properties | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md) | | any | | -| [fields](./kibana-plugin-plugins-data-server.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => FieldSpec[];
} | | -| [fieldsFetcher](./kibana-plugin-plugins-data-server.indexpattern.fieldsfetcher.md) | | any | | -| [flattenHit](./kibana-plugin-plugins-data-server.indexpattern.flattenhit.md) | | any | | -| [formatField](./kibana-plugin-plugins-data-server.indexpattern.formatfield.md) | | any | | -| [formatHit](./kibana-plugin-plugins-data-server.indexpattern.formathit.md) | | any | | +| [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md) | | Record<string, any> | | +| [fields](./kibana-plugin-plugins-data-server.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => IndexPatternFieldMap;
} | | +| [flattenHit](./kibana-plugin-plugins-data-server.indexpattern.flattenhit.md) | | (hit: Record<string, any>, deep?: boolean) => Record<string, any> | | +| [formatField](./kibana-plugin-plugins-data-server.indexpattern.formatfield.md) | | FormatFieldFn | | +| [formatHit](./kibana-plugin-plugins-data-server.indexpattern.formathit.md) | | {
(hit: Record<string, any>, type?: string): any;
formatField: FormatFieldFn;
} | | +| [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md) | | () => {
title?: string | undefined;
timeFieldName?: string | undefined;
intervalName?: string | undefined;
fields?: string | undefined;
sourceFilters?: string | undefined;
fieldFormatMap?: string | undefined;
typeMeta?: string | undefined;
type?: string | undefined;
} | Get last saved saved object fields | | [id](./kibana-plugin-plugins-data-server.indexpattern.id.md) | | string | | | [intervalName](./kibana-plugin-plugins-data-server.indexpattern.intervalname.md) | | string | undefined | | | [metaFields](./kibana-plugin-plugins-data-server.indexpattern.metafields.md) | | string[] | | -| [originalBody](./kibana-plugin-plugins-data-server.indexpattern.originalbody.md) | | {
[key: string]: any;
} | | +| [resetOriginalSavedObjectBody](./kibana-plugin-plugins-data-server.indexpattern.resetoriginalsavedobjectbody.md) | | () => void | Reset last saved saved object fields. used after saving | | [sourceFilters](./kibana-plugin-plugins-data-server.indexpattern.sourcefilters.md) | | SourceFilter[] | | | [timeFieldName](./kibana-plugin-plugins-data-server.indexpattern.timefieldname.md) | | string | undefined | | | [title](./kibana-plugin-plugins-data-server.indexpattern.title.md) | | string | | @@ -41,26 +41,20 @@ export declare class IndexPattern implements IIndexPattern | Method | Modifiers | Description | | --- | --- | --- | -| [\_fetchFields()](./kibana-plugin-plugins-data-server.indexpattern._fetchfields.md) | | | -| [addScriptedField(name, script, fieldType, lang)](./kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md) | | | -| [create(allowOverride)](./kibana-plugin-plugins-data-server.indexpattern.create.md) | | | +| [addScriptedField(name, script, fieldType, lang)](./kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md) | | Add scripted field to field list | | [getAggregationRestrictions()](./kibana-plugin-plugins-data-server.indexpattern.getaggregationrestrictions.md) | | | +| [getAsSavedObjectBody()](./kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md) | | Returns index pattern as saved object body for saving | | [getComputedFields()](./kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md) | | | | [getFieldByName(name)](./kibana-plugin-plugins-data-server.indexpattern.getfieldbyname.md) | | | -| [getFormatterForField(field)](./kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md) | | | +| [getFormatterForField(field)](./kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md) | | Provide a field, get its formatter | | [getNonScriptedFields()](./kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md) | | | | [getScriptedFields()](./kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md) | | | -| [getSourceFiltering()](./kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md) | | | +| [getSourceFiltering()](./kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md) | | Get the source filtering configuration for that index. | | [getTimeField()](./kibana-plugin-plugins-data-server.indexpattern.gettimefield.md) | | | -| [init()](./kibana-plugin-plugins-data-server.indexpattern.init.md) | | | -| [initFromSpec(spec)](./kibana-plugin-plugins-data-server.indexpattern.initfromspec.md) | | | | [isTimeBased()](./kibana-plugin-plugins-data-server.indexpattern.istimebased.md) | | | | [isTimeBasedWildcard()](./kibana-plugin-plugins-data-server.indexpattern.istimebasedwildcard.md) | | | | [isTimeNanosBased()](./kibana-plugin-plugins-data-server.indexpattern.istimenanosbased.md) | | | -| [isWildcard()](./kibana-plugin-plugins-data-server.indexpattern.iswildcard.md) | | | | [popularizeField(fieldName, unit)](./kibana-plugin-plugins-data-server.indexpattern.popularizefield.md) | | | -| [prepBody()](./kibana-plugin-plugins-data-server.indexpattern.prepbody.md) | | | -| [refreshFields()](./kibana-plugin-plugins-data-server.indexpattern.refreshfields.md) | | | -| [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md) | | | +| [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md) | | Remove scripted field from field list | | [toSpec()](./kibana-plugin-plugins-data-server.indexpattern.tospec.md) | | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.originalbody.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.originalbody.md deleted file mode 100644 index b7357d6e85ae7..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.originalbody.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [originalBody](./kibana-plugin-plugins-data-server.indexpattern.originalbody.md) - -## IndexPattern.originalBody property - -Signature: - -```typescript -originalBody: { - [key: string]: any; - }; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.refreshfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.refreshfields.md deleted file mode 100644 index 168e131937eea..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.refreshfields.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [refreshFields](./kibana-plugin-plugins-data-server.indexpattern.refreshfields.md) - -## IndexPattern.refreshFields() method - -Signature: - -```typescript -refreshFields(): Promise; -``` -Returns: - -`Promise` - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md index 8205175485398..3162a7f42dd12 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md @@ -4,6 +4,8 @@ ## IndexPattern.removeScriptedField() method +Remove scripted field from field list + Signature: ```typescript diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.resetoriginalsavedobjectbody.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.resetoriginalsavedobjectbody.md new file mode 100644 index 0000000000000..18ec7070bd577 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.resetoriginalsavedobjectbody.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [resetOriginalSavedObjectBody](./kibana-plugin-plugins-data-server.indexpattern.resetoriginalsavedobjectbody.md) + +## IndexPattern.resetOriginalSavedObjectBody property + +Reset last saved saved object fields. used after saving + +Signature: + +```typescript +resetOriginalSavedObjectBody: () => void; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md index 4a5b61f5c179b..40b029da00469 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md @@ -4,12 +4,6 @@ ## IndexPatternAttributes interface -> Warning: This API is now obsolete. -> -> - -Use data plugin interface instead - Signature: ```typescript diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md new file mode 100644 index 0000000000000..aa78c055f4f5c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) + +## IndexPatternsService class + +Signature: + +```typescript +export declare class IndexPatternsService implements Plugin +``` + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [setup(core)](./kibana-plugin-plugins-data-server.indexpatternsservice.setup.md) | | | +| [start(core, { fieldFormats, logger })](./kibana-plugin-plugins-data-server.indexpatternsservice.start.md) | | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md new file mode 100644 index 0000000000000..a354fbc2a477b --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [setup](./kibana-plugin-plugins-data-server.indexpatternsservice.setup.md) + +## IndexPatternsService.setup() method + +Signature: + +```typescript +setup(core: CoreSetup): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreSetup | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md new file mode 100644 index 0000000000000..d35dc3aa11000 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [start](./kibana-plugin-plugins-data-server.indexpatternsservice.start.md) + +## IndexPatternsService.start() method + +Signature: + +```typescript +start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { + indexPatternsServiceFactory: (kibanaRequest: KibanaRequest) => Promise; + }; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreStart | | +| { fieldFormats, logger } | IndexPatternsServiceStartDeps | | + +Returns: + +`{ + indexPatternsServiceFactory: (kibanaRequest: KibanaRequest) => Promise; + }` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index dea79f5dc4a9f..7113ac935907f 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -11,6 +11,7 @@ | [AggParamType](./kibana-plugin-plugins-data-server.aggparamtype.md) | | | [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) | | | [IndexPatternsFetcher](./kibana-plugin-plugins-data-server.indexpatternsfetcher.md) | | +| [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) | | | [Plugin](./kibana-plugin-plugins-data-server.plugin.md) | | @@ -48,7 +49,7 @@ | [IEsSearchResponse](./kibana-plugin-plugins-data-server.iessearchresponse.md) | | | [IFieldSubType](./kibana-plugin-plugins-data-server.ifieldsubtype.md) | | | [IFieldType](./kibana-plugin-plugins-data-server.ifieldtype.md) | | -| [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) | Use data plugin interface instead | +| [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) | | | [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) | | | [ISearchSetup](./kibana-plugin-plugins-data-server.isearchsetup.md) | | | [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index 455c5ecdd8195..84aeb4cf80cce 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -13,7 +13,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; + indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; }; }; ``` @@ -32,7 +32,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; + indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; }; }` diff --git a/src/fixtures/stubbed_saved_object_index_pattern.ts b/src/fixtures/stubbed_saved_object_index_pattern.ts index 44b391f14cf9c..261e451db5452 100644 --- a/src/fixtures/stubbed_saved_object_index_pattern.ts +++ b/src/fixtures/stubbed_saved_object_index_pattern.ts @@ -28,10 +28,10 @@ export function stubbedSavedObjectIndexPattern(id: string | null = null) { type: 'index-pattern', attributes: { timeFieldName: 'timestamp', - customFormats: '{}', + customFormats: {}, fields: mockLogstashFields, title: 'title', }, - version: 2, + version: '2', }; } diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index bc7080e7d450b..153b6a633b66d 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -27,3 +27,10 @@ export * from './query'; export * from './search'; export * from './types'; export * from './utils'; + +/** + * Use data plugin interface instead + * @deprecated + */ + +export { IndexPatternAttributes } from './types'; diff --git a/src/plugins/data/common/index_patterns/errors.ts b/src/plugins/data/common/index_patterns/errors/duplicate_index_pattern.ts similarity index 76% rename from src/plugins/data/common/index_patterns/errors.ts rename to src/plugins/data/common/index_patterns/errors/duplicate_index_pattern.ts index 3d92bae1968fb..c42dcc1c6a24d 100644 --- a/src/plugins/data/common/index_patterns/errors.ts +++ b/src/plugins/data/common/index_patterns/errors/duplicate_index_pattern.ts @@ -17,13 +17,9 @@ * under the License. */ -import { FieldSpec } from './types'; - -export class FieldTypeUnknownError extends Error { - public readonly fieldSpec: FieldSpec; - constructor(message: string, spec: FieldSpec) { +export class DuplicateIndexPatternError extends Error { + constructor(message: string) { super(message); - this.name = 'FieldTypeUnknownError'; - this.fieldSpec = spec; + this.name = 'DuplicateIndexPatternError'; } } diff --git a/src/plugins/data/common/index_patterns/errors/index.ts b/src/plugins/data/common/index_patterns/errors/index.ts new file mode 100644 index 0000000000000..7cc39d93a2a18 --- /dev/null +++ b/src/plugins/data/common/index_patterns/errors/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './duplicate_index_pattern'; diff --git a/src/plugins/data/common/index_patterns/fields/field_list.ts b/src/plugins/data/common/index_patterns/fields/field_list.ts index 4cf6075869851..c0eb55a15fead 100644 --- a/src/plugins/data/common/index_patterns/fields/field_list.ts +++ b/src/plugins/data/common/index_patterns/fields/field_list.ts @@ -20,7 +20,7 @@ import { findIndex } from 'lodash'; import { IFieldType } from './types'; import { IndexPatternField } from './index_pattern_field'; -import { OnNotification, FieldSpec } from '../types'; +import { FieldSpec, IndexPatternFieldMap } from '../types'; import { IndexPattern } from '../index_patterns'; import { shortenDottedString } from '../../utils'; @@ -35,16 +35,11 @@ export interface IIndexPatternFieldList extends Array { removeAll(): void; replaceAll(specs: FieldSpec[]): void; update(field: FieldSpec): void; - toSpec(options?: { getFormatterForField?: IndexPattern['getFormatterForField'] }): FieldSpec[]; + toSpec(options?: { + getFormatterForField?: IndexPattern['getFormatterForField']; + }): IndexPatternFieldMap; } -export type CreateIndexPatternFieldList = ( - indexPattern: IndexPattern, - specs?: FieldSpec[], - shortDotsEnable?: boolean, - onNotification?: OnNotification -) => IIndexPatternFieldList; - // extending the array class and using a constructor doesn't work well // when calling filter and similar so wrapping in a callback. // to be removed in the future @@ -105,7 +100,7 @@ export const fieldList = ( this.groups.clear(); }; - public readonly replaceAll = (spcs: FieldSpec[]) => { + public readonly replaceAll = (spcs: FieldSpec[] = []) => { this.removeAll(); spcs.forEach(this.add); }; @@ -115,7 +110,12 @@ export const fieldList = ( }: { getFormatterForField?: IndexPattern['getFormatterForField']; } = {}) { - return [...this.map((field) => field.toSpec({ getFormatterForField }))]; + return { + ...this.reduce((collector, field) => { + collector[field.name] = field.toSpec({ getFormatterForField }); + return collector; + }, {}), + }; } } diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index b83323ea19556..808afc3449c2a 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -17,12 +17,9 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; import { KbnFieldType, getKbnFieldType } from '../../kbn_field_types'; -import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { IFieldType } from './types'; import { FieldSpec, IndexPattern } from '../..'; -import { FieldTypeUnknownError } from '../errors'; export class IndexPatternField implements IFieldType { readonly spec: FieldSpec; @@ -35,16 +32,12 @@ export class IndexPatternField implements IFieldType { this.displayName = displayName; this.kbnFieldType = getKbnFieldType(spec.type); - if (spec.type && this.kbnFieldType?.name === KBN_FIELD_TYPES.UNKNOWN) { - const msg = i18n.translate('data.indexPatterns.unknownFieldTypeErrorMsg', { - values: { type: spec.type, name: spec.name }, - defaultMessage: `Field '{name}' Unknown field type '{type}'`, - }); - throw new FieldTypeUnknownError(msg, spec); - } } // writable attrs + /** + * Count is used for field popularity + */ public get count() { return this.spec.count || 0; } @@ -53,6 +46,9 @@ export class IndexPatternField implements IFieldType { this.spec.count = count; } + /** + * Script field code + */ public get script() { return this.spec.script; } @@ -61,6 +57,9 @@ export class IndexPatternField implements IFieldType { this.spec.script = script; } + /** + * Script field language + */ public get lang() { return this.spec.lang; } @@ -69,6 +68,9 @@ export class IndexPatternField implements IFieldType { this.spec.lang = lang; } + /** + * Description of field type conflicts across different indices in the same index pattern + */ public get conflictDescriptions() { return this.spec.conflictDescriptions; } diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap index 1871627da76de..ed84aceb60e5a 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap @@ -2,13 +2,13 @@ exports[`IndexPattern toSpec should match snapshot 1`] = ` Object { - "fields": Array [ - Object { + "fields": Object { + "@tags": Object { "aggregatable": true, "conflictDescriptions": undefined, - "count": 10, + "count": 0, "esTypes": Array [ - "long", + "keyword", ], "format": Object { "id": "number", @@ -17,20 +17,20 @@ Object { }, }, "lang": undefined, - "name": "bytes", + "name": "@tags", "readFromDocValues": true, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "number", + "type": "string", }, - Object { + "@timestamp": Object { "aggregatable": true, "conflictDescriptions": undefined, - "count": 20, + "count": 30, "esTypes": Array [ - "boolean", + "date", ], "format": Object { "id": "number", @@ -39,20 +39,20 @@ Object { }, }, "lang": undefined, - "name": "ssl", + "name": "@timestamp", "readFromDocValues": true, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "boolean", + "type": "date", }, - Object { + "_id": Object { "aggregatable": true, "conflictDescriptions": undefined, - "count": 30, + "count": 0, "esTypes": Array [ - "date", + "_id", ], "format": Object { "id": "number", @@ -61,20 +61,20 @@ Object { }, }, "lang": undefined, - "name": "@timestamp", - "readFromDocValues": true, + "name": "_id", + "readFromDocValues": false, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "date", + "type": "string", }, - Object { + "_source": Object { "aggregatable": true, "conflictDescriptions": undefined, - "count": 30, + "count": 0, "esTypes": Array [ - "date", + "_source", ], "format": Object { "id": "number", @@ -83,20 +83,20 @@ Object { }, }, "lang": undefined, - "name": "time", - "readFromDocValues": true, + "name": "_source", + "readFromDocValues": false, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "date", + "type": "_source", }, - Object { + "_type": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "keyword", + "_type", ], "format": Object { "id": "number", @@ -105,20 +105,20 @@ Object { }, }, "lang": undefined, - "name": "@tags", - "readFromDocValues": true, + "name": "_type", + "readFromDocValues": false, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, "type": "string", }, - Object { + "area": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "date", + "geo_shape", ], "format": Object { "id": "number", @@ -127,20 +127,20 @@ Object { }, }, "lang": undefined, - "name": "utc_time", - "readFromDocValues": true, + "name": "area", + "readFromDocValues": false, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "date", + "type": "geo_shape", }, - Object { + "bytes": Object { "aggregatable": true, "conflictDescriptions": undefined, - "count": 0, + "count": 10, "esTypes": Array [ - "integer", + "long", ], "format": Object { "id": "number", @@ -149,7 +149,7 @@ Object { }, }, "lang": undefined, - "name": "phpmemory", + "name": "bytes", "readFromDocValues": true, "script": undefined, "scripted": false, @@ -157,12 +157,12 @@ Object { "subType": undefined, "type": "number", }, - Object { + "custom_user_field": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "ip", + "conflict", ], "format": Object { "id": "number", @@ -171,20 +171,20 @@ Object { }, }, "lang": undefined, - "name": "ip", + "name": "custom_user_field", "readFromDocValues": true, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "ip", + "type": "conflict", }, - Object { + "extension": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "attachment", + "text", ], "format": Object { "id": "number", @@ -193,15 +193,41 @@ Object { }, }, "lang": undefined, - "name": "request_body", - "readFromDocValues": true, + "name": "extension", + "readFromDocValues": false, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "attachment", + "type": "string", }, - Object { + "extension.keyword": Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "keyword", + ], + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, + "lang": undefined, + "name": "extension.keyword", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": Object { + "multi": Object { + "parent": "extension", + }, + }, + "type": "string", + }, + "geo.coordinates": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, @@ -215,7 +241,7 @@ Object { }, }, "lang": undefined, - "name": "point", + "name": "geo.coordinates", "readFromDocValues": true, "script": undefined, "scripted": false, @@ -223,12 +249,12 @@ Object { "subType": undefined, "type": "geo_point", }, - Object { + "geo.src": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "geo_shape", + "keyword", ], "format": Object { "id": "number", @@ -237,15 +263,15 @@ Object { }, }, "lang": undefined, - "name": "area", - "readFromDocValues": false, + "name": "geo.src", + "readFromDocValues": true, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "geo_shape", + "type": "string", }, - Object { + "hashed": Object { "aggregatable": false, "conflictDescriptions": undefined, "count": 0, @@ -267,12 +293,12 @@ Object { "subType": undefined, "type": "murmur3", }, - Object { + "ip": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "geo_point", + "ip", ], "format": Object { "id": "number", @@ -281,15 +307,15 @@ Object { }, }, "lang": undefined, - "name": "geo.coordinates", + "name": "ip", "readFromDocValues": true, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "geo_point", + "type": "ip", }, - Object { + "machine.os": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, @@ -303,7 +329,7 @@ Object { }, }, "lang": undefined, - "name": "extension", + "name": "machine.os", "readFromDocValues": false, "script": undefined, "scripted": false, @@ -311,7 +337,7 @@ Object { "subType": undefined, "type": "string", }, - Object { + "machine.os.raw": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, @@ -325,19 +351,19 @@ Object { }, }, "lang": undefined, - "name": "extension.keyword", + "name": "machine.os.raw", "readFromDocValues": true, "script": undefined, "scripted": false, "searchable": true, "subType": Object { "multi": Object { - "parent": "extension", + "parent": "machine.os", }, }, "type": "string", }, - Object { + "non-filterable": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, @@ -351,20 +377,20 @@ Object { }, }, "lang": undefined, - "name": "machine.os", + "name": "non-filterable", "readFromDocValues": false, "script": undefined, "scripted": false, - "searchable": true, + "searchable": false, "subType": undefined, "type": "string", }, - Object { - "aggregatable": true, + "non-sortable": Object { + "aggregatable": false, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "keyword", + "text", ], "format": Object { "id": "number", @@ -373,24 +399,20 @@ Object { }, }, "lang": undefined, - "name": "machine.os.raw", - "readFromDocValues": true, + "name": "non-sortable", + "readFromDocValues": false, "script": undefined, "scripted": false, - "searchable": true, - "subType": Object { - "multi": Object { - "parent": "machine.os", - }, - }, + "searchable": false, + "subType": undefined, "type": "string", }, - Object { + "phpmemory": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "keyword", + "integer", ], "format": Object { "id": "number", @@ -399,20 +421,20 @@ Object { }, }, "lang": undefined, - "name": "geo.src", + "name": "phpmemory", "readFromDocValues": true, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "string", + "type": "number", }, - Object { + "point": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "_id", + "geo_point", ], "format": Object { "id": "number", @@ -421,20 +443,20 @@ Object { }, }, "lang": undefined, - "name": "_id", - "readFromDocValues": false, + "name": "point", + "readFromDocValues": true, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "string", + "type": "geo_point", }, - Object { + "request_body": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "_type", + "attachment", ], "format": Object { "id": "number", @@ -443,20 +465,20 @@ Object { }, }, "lang": undefined, - "name": "_type", - "readFromDocValues": false, + "name": "request_body", + "readFromDocValues": true, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "string", + "type": "attachment", }, - Object { + "script date": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "_source", + "date", ], "format": Object { "id": "number", @@ -464,43 +486,21 @@ Object { "pattern": "$0,0.[00]", }, }, - "lang": undefined, - "name": "_source", + "lang": "painless", + "name": "script date", "readFromDocValues": false, - "script": undefined, - "scripted": false, + "script": "1234", + "scripted": true, "searchable": true, "subType": undefined, - "type": "_source", + "type": "date", }, - Object { + "script murmur3": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "text", - ], - "format": Object { - "id": "number", - "params": Object { - "pattern": "$0,0.[00]", - }, - }, - "lang": undefined, - "name": "non-filterable", - "readFromDocValues": false, - "script": undefined, - "scripted": false, - "searchable": false, - "subType": undefined, - "type": "string", - }, - Object { - "aggregatable": false, - "conflictDescriptions": undefined, - "count": 0, - "esTypes": Array [ - "text", + "murmur3", ], "format": Object { "id": "number", @@ -508,21 +508,21 @@ Object { "pattern": "$0,0.[00]", }, }, - "lang": undefined, - "name": "non-sortable", + "lang": "expression", + "name": "script murmur3", "readFromDocValues": false, - "script": undefined, - "scripted": false, - "searchable": false, + "script": "1234", + "scripted": true, + "searchable": true, "subType": undefined, - "type": "string", + "type": "murmur3", }, - Object { + "script number": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "conflict", + "long", ], "format": Object { "id": "number", @@ -530,16 +530,16 @@ Object { "pattern": "$0,0.[00]", }, }, - "lang": undefined, - "name": "custom_user_field", - "readFromDocValues": true, - "script": undefined, - "scripted": false, + "lang": "expression", + "name": "script number", + "readFromDocValues": false, + "script": "1234", + "scripted": true, "searchable": true, "subType": undefined, - "type": "conflict", + "type": "number", }, - Object { + "script string": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, @@ -561,12 +561,12 @@ Object { "subType": undefined, "type": "string", }, - Object { + "ssl": Object { "aggregatable": true, "conflictDescriptions": undefined, - "count": 0, + "count": 20, "esTypes": Array [ - "long", + "boolean", ], "format": Object { "id": "number", @@ -574,19 +574,19 @@ Object { "pattern": "$0,0.[00]", }, }, - "lang": "expression", - "name": "script number", - "readFromDocValues": false, - "script": "1234", - "scripted": true, + "lang": undefined, + "name": "ssl", + "readFromDocValues": true, + "script": undefined, + "scripted": false, "searchable": true, "subType": undefined, - "type": "number", + "type": "boolean", }, - Object { + "time": Object { "aggregatable": true, "conflictDescriptions": undefined, - "count": 0, + "count": 30, "esTypes": Array [ "date", ], @@ -596,21 +596,21 @@ Object { "pattern": "$0,0.[00]", }, }, - "lang": "painless", - "name": "script date", - "readFromDocValues": false, - "script": "1234", - "scripted": true, + "lang": undefined, + "name": "time", + "readFromDocValues": true, + "script": undefined, + "scripted": false, "searchable": true, "subType": undefined, "type": "date", }, - Object { + "utc_time": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "murmur3", + "date", ], "format": Object { "id": "number", @@ -618,21 +618,22 @@ Object { "pattern": "$0,0.[00]", }, }, - "lang": "expression", - "name": "script murmur3", - "readFromDocValues": false, - "script": "1234", - "scripted": true, + "lang": undefined, + "name": "utc_time", + "readFromDocValues": true, + "script": undefined, + "scripted": false, "searchable": true, "subType": undefined, - "type": "murmur3", + "type": "date", }, - ], + }, "id": "test-pattern", "sourceFilters": undefined, "timeFieldName": "timestamp", "title": "title", + "type": "index-pattern", "typeMeta": undefined, - "version": 2, + "version": "2", } `; diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap new file mode 100644 index 0000000000000..752fdcf11991c --- /dev/null +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IndexPatterns savedObjectToSpec 1`] = ` +Object { + "fields": Object {}, + "id": "id", + "intervalName": undefined, + "sourceFilters": Array [ + Object { + "value": "item1", + }, + Object { + "value": "item2", + }, + ], + "timeFieldName": "@timestamp", + "title": "kibana-*", + "type": "", + "typeMeta": Object {}, + "version": "version", +} +`; diff --git a/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts b/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts deleted file mode 100644 index 4eba0576ff235..0000000000000 --- a/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { IndexPattern } from '.'; -import { GetFieldsOptions, IIndexPatternsApiClient } from '../types'; - -/** @internal */ -export const createFieldsFetcher = ( - indexPattern: IndexPattern, - apiClient: IIndexPatternsApiClient, - metaFields: string[] = [] -) => { - const fieldFetcher = { - fetch: (options: GetFieldsOptions) => { - return fieldFetcher.fetchForWildcard(indexPattern.title, { - ...options, - type: indexPattern.type, - params: indexPattern.typeMeta && indexPattern.typeMeta.params, - }); - }, - fetchForWildcard: (pattern: string, options: GetFieldsOptions = {}) => { - return apiClient.getFieldsForWildcard({ - pattern, - metaFields, - type: options.type, - params: options.params || {}, - }); - }, - }; - - return fieldFetcher; -}; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index f49897c47d562..91286a38f16a0 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -17,19 +17,18 @@ * under the License. */ -import { defaults, map, last } from 'lodash'; +import { map, last } from 'lodash'; import { IndexPattern } from './index_pattern'; import { DuplicateField } from '../../../../kibana_utils/common'; -// @ts-ignore +// @ts-expect-error import mockLogStashFields from '../../../../../fixtures/logstash_fields'; -// @ts-ignore import { stubbedSavedObjectIndexPattern } from '../../../../../fixtures/stubbed_saved_object_index_pattern'; import { IndexPatternField } from '../fields'; import { fieldFormatsMock } from '../../field_formats/mocks'; -import { FieldFormat, IndexPatternsService } from '../..'; +import { FieldFormat } from '../..'; class MockFieldFormatter {} @@ -63,90 +62,33 @@ jest.mock('../../field_mapping', () => { }; }); -let mockFieldsFetcherResponse: any[] = []; - -jest.mock('./_fields_fetcher', () => ({ - createFieldsFetcher: jest.fn().mockImplementation(() => ({ - fetch: jest.fn().mockImplementation(() => { - return new Promise((resolve) => resolve(mockFieldsFetcherResponse)); - }), - every: jest.fn(), - })), -})); - -let object: any = {}; - -const savedObjectsClient = { - create: jest.fn(), - get: jest.fn().mockImplementation(() => object), - update: jest.fn().mockImplementation(async (type, id, body, { version }) => { - if (object.version !== version) { - throw new Object({ - res: { - status: 409, - }, - }); - } - object.attributes.title = body.title; - object.version += 'a'; - return { - id: object.id, - version: object.version, - }; - }), -}; - -const patternCache = { - clear: jest.fn(), - get: jest.fn(), - set: jest.fn(), - clearAll: jest.fn(), -}; - -const apiClient = { - _getUrl: jest.fn(), - getFieldsForTimePattern: jest.fn(), - getFieldsForWildcard: jest.fn(), -}; - // helper function to create index patterns -function create(id: string, payload?: any): Promise { - const indexPattern = new IndexPattern(id, { - savedObjectsClient: savedObjectsClient as any, - apiClient, - patternCache, +function create(id: string) { + const { + type, + version, + attributes: { timeFieldName, fields, title }, + } = stubbedSavedObjectIndexPattern(id); + + return new IndexPattern({ + spec: { id, type, version, timeFieldName, fields, title }, + savedObjectsClient: {} as any, fieldFormats: fieldFormatsMock, - indexPatternsService: {} as IndexPatternsService, - onNotification: () => {}, - onError: () => {}, shortDotsEnable: false, metaFields: [], }); - - setDocsourcePayload(id, payload); - - return indexPattern.init(); -} - -function setDocsourcePayload(id: string | null, providedPayload: any) { - object = defaults(providedPayload || {}, stubbedSavedObjectIndexPattern(id)); } describe('IndexPattern', () => { - const indexPatternId = 'test-pattern'; - let indexPattern: IndexPattern; // create an indexPattern instance for each test beforeEach(() => { - return create(indexPatternId).then((pattern: IndexPattern) => { - indexPattern = pattern; - }); + indexPattern = create('test-pattern'); }); describe('api', () => { test('should have expected properties', () => { - expect(indexPattern).toHaveProperty('refreshFields'); expect(indexPattern).toHaveProperty('popularizeField'); expect(indexPattern).toHaveProperty('getScriptedFields'); expect(indexPattern).toHaveProperty('getNonScriptedFields'); @@ -158,13 +100,6 @@ describe('IndexPattern', () => { }); }); - describe('init', () => { - test('should append the found fields', () => { - expect(savedObjectsClient.get).toHaveBeenCalled(); - expect(indexPattern.fields).toHaveLength(mockLogStashFields().length); - }); - }); - describe('fields', () => { test('should have expected properties on fields', function () { expect(indexPattern.fields[0]).toHaveProperty('displayName'); @@ -229,43 +164,9 @@ describe('IndexPattern', () => { }); }); - describe('refresh fields', () => { - test('should fetch fields from the fieldsFetcher', async () => { - expect(indexPattern.fields.length).toBeGreaterThan(2); - - mockFieldsFetcherResponse = [{ name: 'foo' }, { name: 'bar' }]; - - await indexPattern.refreshFields(); - - mockFieldsFetcherResponse = []; - - const newFields = indexPattern.getNonScriptedFields(); - - expect(newFields).toHaveLength(2); - expect([...newFields.map((f) => f.name)]).toEqual(['foo', 'bar']); - }); - - test('should preserve the scripted fields', async () => { - // add spy to indexPattern.getScriptedFields - // sinon.spy(indexPattern, 'getScriptedFields'); - - // refresh fields, which will fetch - await indexPattern.refreshFields(); - - // called to append scripted fields to the response from mapper.getFieldsForIndexPattern - // sinon.assert.calledOnce(indexPattern.getScriptedFields); - expect(indexPattern.getScriptedFields().map((f) => f.name)).toEqual( - mockLogStashFields() - .filter((f: IndexPatternField) => f.scripted) - .map((f: IndexPatternField) => f.name) - ); - }); - }); - describe('add and remove scripted fields', () => { test('should append the scripted field', async () => { // keep a copy of the current scripted field count - // const saveSpy = sinon.spy(indexPattern, 'save'); const oldCount = indexPattern.getScriptedFields().length; // add a new scripted field @@ -283,7 +184,6 @@ describe('IndexPattern', () => { ); const scriptedFields = indexPattern.getScriptedFields(); - // expect(saveSpy.callCount).to.equal(1); expect(scriptedFields).toHaveLength(oldCount + 1); expect((indexPattern.fields.getByName(scriptedField.name) as IndexPatternField).name).toEqual( scriptedField.name @@ -291,14 +191,12 @@ describe('IndexPattern', () => { }); test('should remove scripted field, by name', async () => { - // const saveSpy = sinon.spy(indexPattern, 'save'); const scriptedFields = indexPattern.getScriptedFields(); const oldCount = scriptedFields.length; const scriptedField = last(scriptedFields)!; await indexPattern.removeScriptedField(scriptedField.name); - // expect(saveSpy.callCount).to.equal(1); expect(indexPattern.getScriptedFields().length).toEqual(oldCount - 1); expect(indexPattern.fields.getByName(scriptedField.name)).toEqual(undefined); }); @@ -330,8 +228,13 @@ describe('IndexPattern', () => { } as FieldFormat; indexPattern.getFormatterForField = () => formatter; const spec = indexPattern.toSpec(); - const restoredPattern = await create(spec.id as string); - restoredPattern.initFromSpec(spec); + const restoredPattern = new IndexPattern({ + spec, + savedObjectsClient: {} as any, + fieldFormats: fieldFormatsMock, + shortDotsEnable: false, + metaFields: [], + }); expect(restoredPattern.id).toEqual(indexPattern.id); expect(restoredPattern.title).toEqual(indexPattern.title); expect(restoredPattern.timeFieldName).toEqual(indexPattern.timeFieldName); @@ -342,26 +245,22 @@ describe('IndexPattern', () => { describe('popularizeField', () => { test('should increment the popularity count by default', () => { - // const saveSpy = sinon.stub(indexPattern, 'save'); indexPattern.fields.forEach(async (field) => { const oldCount = field.count || 0; await indexPattern.popularizeField(field.name); - // expect(saveSpy.callCount).to.equal(i + 1); expect(field.count).toEqual(oldCount + 1); }); }); test('should increment the popularity count', () => { - // const saveSpy = sinon.stub(indexPattern, 'save'); indexPattern.fields.forEach(async (field) => { const oldCount = field.count || 0; const incrementAmount = 4; await indexPattern.popularizeField(field.name, incrementAmount); - // expect(saveSpy.callCount).to.equal(i + 1); expect(field.count).toEqual(oldCount + incrementAmount); }); }); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index b2ecca04ca8df..882235889b55c 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -18,212 +18,94 @@ */ import _, { each, reject } from 'lodash'; -import { i18n } from '@kbn/i18n'; import { SavedObjectsClientCommon } from '../..'; -import { DuplicateField, SavedObjectNotFound } from '../../../../kibana_utils/common'; +import { DuplicateField } from '../../../../kibana_utils/common'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, - FieldTypeUnknownError, FieldFormatNotFoundError, IFieldType, } from '../../../common'; -import { findByTitle } from '../utils'; -import { IndexPatternMissingIndices } from '../lib'; import { IndexPatternField, IIndexPatternFieldList, fieldList } from '../fields'; -import { createFieldsFetcher } from './_fields_fetcher'; import { formatHitProvider } from './format_hit'; import { flattenHitWrapper } from './flatten_hit'; -import { OnNotification, OnError, IIndexPatternsApiClient, IndexPatternAttributes } from '../types'; import { FieldFormatsStartCommon, FieldFormat } from '../../field_formats'; -import { PatternCache } from './_pattern_cache'; -import { expandShorthand, FieldMappingSpec, MappingObject } from '../../field_mapping'; -import { IndexPatternSpec, TypeMeta, FieldSpec, SourceFilter } from '../types'; +import { IndexPatternSpec, TypeMeta, SourceFilter, IndexPatternFieldMap } from '../types'; import { SerializedFieldFormat } from '../../../../expressions/common'; -import { IndexPatternsService } from '..'; - -const savedObjectType = 'index-pattern'; interface IndexPatternDeps { + spec?: IndexPatternSpec; savedObjectsClient: SavedObjectsClientCommon; - apiClient: IIndexPatternsApiClient; - patternCache: PatternCache; fieldFormats: FieldFormatsStartCommon; - indexPatternsService: IndexPatternsService; - onNotification: OnNotification; - onError: OnError; shortDotsEnable: boolean; metaFields: string[]; } +interface SavedObjectBody { + title?: string; + timeFieldName?: string; + intervalName?: string; + fields?: string; + sourceFilters?: string; + fieldFormatMap?: string; + typeMeta?: string; + type?: string; +} + +type FormatFieldFn = (hit: Record, fieldName: string) => any; + export class IndexPattern implements IIndexPattern { public id?: string; public title: string = ''; - public fieldFormatMap: any; + public fieldFormatMap: Record; public typeMeta?: TypeMeta; - public fields: IIndexPatternFieldList & { toSpec: () => FieldSpec[] }; + public fields: IIndexPatternFieldList & { toSpec: () => IndexPatternFieldMap }; public timeFieldName: string | undefined; public intervalName: string | undefined; public type: string | undefined; - public formatHit: any; - public formatField: any; - public flattenHit: any; + public formatHit: { + (hit: Record, type?: string): any; + formatField: FormatFieldFn; + }; + public formatField: FormatFieldFn; + public flattenHit: (hit: Record, deep?: boolean) => Record; public metaFields: string[]; - + // savedObject version public version: string | undefined; private savedObjectsClient: SavedObjectsClientCommon; - private patternCache: PatternCache; public sourceFilters?: SourceFilter[]; - // todo make read only, update via method or factor out - public originalBody: { [key: string]: any } = {}; - public fieldsFetcher: any; // probably want to factor out any direct usage and change to private - private indexPatternsService: IndexPatternsService; + private originalSavedObjectBody: SavedObjectBody = {}; private shortDotsEnable: boolean = false; private fieldFormats: FieldFormatsStartCommon; - private onNotification: OnNotification; - private onError: OnError; - - private mapping: MappingObject = expandShorthand({ - title: ES_FIELD_TYPES.TEXT, - timeFieldName: ES_FIELD_TYPES.KEYWORD, - intervalName: ES_FIELD_TYPES.KEYWORD, - fields: 'json', - sourceFilters: 'json', - fieldFormatMap: { - type: ES_FIELD_TYPES.TEXT, - _serialize: (map = {}) => { - const serialized = _.transform(map, this.serializeFieldFormatMap); - return _.isEmpty(serialized) ? undefined : JSON.stringify(serialized); - }, - _deserialize: (map = '{}') => { - return _.mapValues(JSON.parse(map), (mapping) => { - return this.deserializeFieldFormatMap(mapping); - }); - }, - }, - type: ES_FIELD_TYPES.KEYWORD, - typeMeta: 'json', - }); - - constructor( - id: string | undefined, - { - savedObjectsClient, - apiClient, - patternCache, - fieldFormats, - indexPatternsService, - onNotification, - onError, - shortDotsEnable = false, - metaFields = [], - }: IndexPatternDeps - ) { - this.id = id; + + constructor({ + spec = {}, + savedObjectsClient, + fieldFormats, + shortDotsEnable = false, + metaFields = [], + }: IndexPatternDeps) { + // set dependencies this.savedObjectsClient = savedObjectsClient; - this.patternCache = patternCache; this.fieldFormats = fieldFormats; - this.indexPatternsService = indexPatternsService; - this.onNotification = onNotification; - this.onError = onError; - + // set config this.shortDotsEnable = shortDotsEnable; this.metaFields = metaFields; + // initialize functionality this.fields = fieldList([], this.shortDotsEnable); - this.fieldsFetcher = createFieldsFetcher(this, apiClient, metaFields); this.flattenHit = flattenHitWrapper(this, metaFields); this.formatHit = formatHitProvider( this, fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.STRING) ); this.formatField = this.formatHit.formatField; - } - - private unknownFieldErrorNotification( - fieldType: string, - fieldName: string, - indexPatternTitle: string - ) { - const title = i18n.translate('data.indexPatterns.unknownFieldHeader', { - values: { type: fieldType }, - defaultMessage: 'Unknown field type {type}', - }); - const text = i18n.translate('data.indexPatterns.unknownFieldErrorMessage', { - values: { name: fieldName, title: indexPatternTitle }, - defaultMessage: 'Field {name} in indexPattern {title} is using an unknown field type.', - }); - this.onNotification({ title, text, color: 'danger', iconType: 'alert' }); - } - - private serializeFieldFormatMap(flat: any, format: string, field: string | undefined) { - if (format && field) { - flat[field] = format; - } - } - - private deserializeFieldFormatMap(mapping: any) { - try { - return this.fieldFormats.getInstance(mapping.id, mapping.params); - } catch (err) { - if (err instanceof FieldFormatNotFoundError) { - return undefined; - } else { - throw err; - } - } - } - - private isFieldRefreshRequired(specs?: FieldSpec[]): boolean { - if (!specs) { - return true; - } - - return specs.every((spec) => { - // See https://github.com/elastic/kibana/pull/8421 - const hasFieldCaps = 'aggregatable' in spec && 'searchable' in spec; - - // See https://github.com/elastic/kibana/pull/11969 - const hasDocValuesFlag = 'readFromDocValues' in spec; - - return !hasFieldCaps || !hasDocValuesFlag; - }); - } - - private async indexFields(specs?: FieldSpec[]) { - if (!this.id) { - return; - } - - if (this.isFieldRefreshRequired(specs)) { - await this.refreshFields(); - } else { - if (specs) { - try { - this.fields.replaceAll(specs); - } catch (err) { - if (err instanceof FieldTypeUnknownError) { - this.unknownFieldErrorNotification(err.fieldSpec.name, err.fieldSpec.type, this.title); - } else { - throw err; - } - } - } - } - } - public initFromSpec(spec: IndexPatternSpec) { - // create fieldFormatMap from field list - const fieldFormatMap: Record = {}; - if (_.isArray(spec.fields)) { - spec.fields.forEach((field: FieldSpec) => { - if (field.format) { - fieldFormatMap[field.name as string] = { ...field.format }; - } - }); - } + // set values + this.id = spec.id; + const fieldFormatMap = this.fieldSpecsToFieldFormatMap(spec.fields); this.version = spec.version; @@ -231,53 +113,55 @@ export class IndexPattern implements IIndexPattern { this.timeFieldName = spec.timeFieldName; this.sourceFilters = spec.sourceFilters; - try { - this.fields.replaceAll(spec.fields || []); - } catch (err) { - if (err instanceof FieldTypeUnknownError) { - this.unknownFieldErrorNotification(err.fieldSpec.name, err.fieldSpec.type, this.title); - } else { - throw err; - } - } + this.fields.replaceAll(Object.values(spec.fields || {})); + this.type = spec.type; this.typeMeta = spec.typeMeta; this.fieldFormatMap = _.mapValues(fieldFormatMap, (mapping) => { return this.deserializeFieldFormatMap(mapping); }); - - return this; } - private updateFromElasticSearch(response: any) { - if (!response.found) { - throw new SavedObjectNotFound(savedObjectType, this.id, 'management/kibana/indexPatterns'); - } - - _.forOwn(this.mapping, (fieldMapping: FieldMappingSpec, name: string | undefined) => { - if (!fieldMapping._deserialize || !name) { - return; + /** + * Get last saved saved object fields + */ + getOriginalSavedObjectBody = () => ({ ...this.originalSavedObjectBody }); + + /** + * Reset last saved saved object fields. used after saving + */ + resetOriginalSavedObjectBody = () => { + this.originalSavedObjectBody = this.getAsSavedObjectBody(); + }; + + /** + * Converts field format spec to field format instance + * @param mapping + */ + private deserializeFieldFormatMap(mapping: SerializedFieldFormat>) { + try { + return this.fieldFormats.getInstance(mapping.id as string, mapping.params); + } catch (err) { + if (err instanceof FieldFormatNotFoundError) { + return undefined; + } else { + throw err; } - - response[name] = fieldMapping._deserialize(response[name]); - }); - - this.title = response.title; - this.timeFieldName = response.timeFieldName; - this.intervalName = response.intervalName; - this.sourceFilters = response.sourceFilters; - this.fieldFormatMap = response.fieldFormatMap; - this.type = response.type; - this.typeMeta = response.typeMeta; - - if (!this.title && this.id) { - this.title = this.id; } - this.version = response.version; - - return this.indexFields(response.fields); } + /** + * Extracts FieldFormatMap from FieldSpec map + * @param fldList FieldSpec map + */ + private fieldSpecsToFieldFormatMap = (fldList: IndexPatternSpec['fields'] = {}) => + Object.values(fldList).reduce>((col, fieldSpec) => { + if (fieldSpec.format) { + col[fieldSpec.name] = { ...fieldSpec.format }; + } + return col; + }, {}); + getComputedFields() { const scriptFields: any = {}; if (!this.fields) { @@ -319,37 +203,6 @@ export class IndexPattern implements IIndexPattern { }; } - async init() { - if (!this.id) { - return this; // no id === no elasticsearch document - } - - const savedObject = await this.savedObjectsClient.get( - savedObjectType, - this.id - ); - - const response = { - version: savedObject.version, - found: savedObject.version ? true : false, - title: savedObject.attributes.title, - timeFieldName: savedObject.attributes.timeFieldName, - intervalName: savedObject.attributes.intervalName, - fields: savedObject.attributes.fields, - sourceFilters: savedObject.attributes.sourceFilters, - fieldFormatMap: savedObject.attributes.fieldFormatMap, - typeMeta: savedObject.attributes.typeMeta, - type: savedObject.attributes.type, - }; - // Do this before we attempt to update from ES since that call can potentially perform a save - this.originalBody = this.prepBody(); - await this.updateFromElasticSearch(response); - // Do it after to ensure we have the most up to date information - this.originalBody = this.prepBody(); - - return this; - } - public toSpec(): IndexPatternSpec { return { id: this.id, @@ -360,17 +213,33 @@ export class IndexPattern implements IIndexPattern { sourceFilters: this.sourceFilters, fields: this.fields.toSpec({ getFormatterForField: this.getFormatterForField.bind(this) }), typeMeta: this.typeMeta, + type: this.type, }; } - // Get the source filtering configuration for that index. + /** + * Get the source filtering configuration for that index. + */ getSourceFiltering() { return { excludes: (this.sourceFilters && this.sourceFilters.map((filter: any) => filter.value)) || [], }; } - async addScriptedField(name: string, script: string, fieldType: string = 'string', lang: string) { + /** + * Add scripted field to field list + * + * @param name field name + * @param script script code + * @param fieldType + * @param lang + */ + async addScriptedField( + name: string, + script: string, + fieldType: string = 'string', + lang: string = 'painless' + ) { const scriptedFields = this.getScriptedFields(); const names = _.map(scriptedFields, 'name'); @@ -378,27 +247,24 @@ export class IndexPattern implements IIndexPattern { throw new DuplicateField(name); } - try { - this.fields.add({ - name, - script, - type: fieldType, - scripted: true, - lang, - aggregatable: true, - searchable: true, - count: 0, - readFromDocValues: false, - }); - } catch (err) { - if (err instanceof FieldTypeUnknownError) { - this.unknownFieldErrorNotification(err.fieldSpec.name, err.fieldSpec.type, this.title); - } else { - throw err; - } - } + this.fields.add({ + name, + script, + type: fieldType, + scripted: true, + lang, + aggregatable: true, + searchable: true, + count: 0, + readFromDocValues: false, + }); } + /** + * Remove scripted field from field list + * @param fieldName + */ + removeScriptedField(fieldName: string) { const field = this.fields.getByName(fieldName); if (field) { @@ -425,9 +291,14 @@ export class IndexPattern implements IIndexPattern { field.count = count; try { - const res = await this.savedObjectsClient.update(savedObjectType, this.id, this.prepBody(), { - version: this.version, - }); + const res = await this.savedObjectsClient.update( + 'index-pattern', + this.id, + this.getAsSavedObjectBody(), + { + version: this.version, + } + ); this.version = res.version; } catch (e) { // no need for an error message here @@ -469,23 +340,45 @@ export class IndexPattern implements IIndexPattern { return this.typeMeta?.aggs; } - isWildcard() { + /** + * Does this index pattern title include a '*' + */ + private isWildcard() { return _.includes(this.title, '*'); } - prepBody() { + /** + * Returns index pattern as saved object body for saving + */ + getAsSavedObjectBody() { + const serializeFieldFormatMap = ( + flat: any, + format: FieldFormat | undefined, + field: string | undefined + ) => { + if (format && field) { + flat[field] = format; + } + }; + const serialized = _.transform(this.fieldFormatMap, serializeFieldFormatMap); + const fieldFormatMap = _.isEmpty(serialized) ? undefined : JSON.stringify(serialized); + return { title: this.title, timeFieldName: this.timeFieldName, intervalName: this.intervalName, - sourceFilters: this.mapping.sourceFilters._serialize!(this.sourceFilters), - fields: this.mapping.fields._serialize!(this.fields), - fieldFormatMap: this.mapping.fieldFormatMap._serialize!(this.fieldFormatMap), + sourceFilters: this.sourceFilters ? JSON.stringify(this.sourceFilters) : undefined, + fields: this.fields ? JSON.stringify(this.fields) : undefined, + fieldFormatMap, type: this.type, - typeMeta: this.mapping.typeMeta._serialize!(this.typeMeta), + typeMeta: this.typeMeta ? JSON.stringify(this.typeMeta) : undefined, }; } + /** + * Provide a field, get its formatter + * @param field + */ getFormatterForField( field: IndexPatternField | IndexPatternField['spec'] | IFieldType ): FieldFormat { @@ -497,81 +390,4 @@ export class IndexPattern implements IIndexPattern { ) ); } - - async create(allowOverride: boolean = false) { - const _create = async (duplicateId?: string) => { - if (duplicateId) { - this.patternCache.clear(duplicateId); - await this.savedObjectsClient.delete(savedObjectType, duplicateId); - } - - const body = this.prepBody(); - const response = await this.savedObjectsClient.create(savedObjectType, body, { id: this.id }); - - this.id = response.id; - return response.id; - }; - - const potentialDuplicateByTitle = await findByTitle(this.savedObjectsClient, this.title); - // If there is potentially duplicate title, just create it - if (!potentialDuplicateByTitle) { - return await _create(); - } - - // We found a duplicate but we aren't allowing override, show the warn modal - if (!allowOverride) { - return false; - } - - return await _create(potentialDuplicateByTitle.id); - } - - async _fetchFields() { - const fields = await this.fieldsFetcher.fetch(this); - const scripted = this.getScriptedFields().map((field) => field.spec); - try { - this.fields.replaceAll([...fields, ...scripted]); - } catch (err) { - if (err instanceof FieldTypeUnknownError) { - this.unknownFieldErrorNotification(err.fieldSpec.name, err.fieldSpec.type, this.title); - } else { - throw err; - } - } - } - - refreshFields() { - return ( - this._fetchFields() - // todo - .then(() => this.indexPatternsService.save(this)) - .catch((err) => { - // https://github.com/elastic/kibana/issues/9224 - // This call will attempt to remap fields from the matching - // ES index which may not actually exist. In that scenario, - // we still want to notify the user that there is a problem - // but we do not want to potentially make any pages unusable - // so do not rethrow the error here - - if (err instanceof IndexPatternMissingIndices) { - this.onNotification({ - title: (err as any).message, - color: 'danger', - iconType: 'alert', - }); - return []; - } - - this.onError(err, { - title: i18n.translate('data.indexPatterns.fetchFieldErrorTitle', { - defaultMessage: 'Error fetching fields for index pattern {title} (ID: {id})', - values: { - id: this.id, - title: this.title, - }, - }), - }); - }) - ); - } } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts index d3b3a73a4b50f..b22437ebbdb4e 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts @@ -18,7 +18,7 @@ */ import { defaults } from 'lodash'; -import { IndexPatternsService } from '.'; +import { IndexPatternsService, IndexPattern } from '.'; import { fieldFormatsMock } from '../../field_formats/mocks'; import { stubbedSavedObjectIndexPattern } from '../../../../../fixtures/stubbed_saved_object_index_pattern'; import { UiSettingsCommon, SavedObjectsClientCommon, SavedObject } from '../types'; @@ -31,7 +31,6 @@ const createFieldsFetcher = jest.fn().mockImplementation(() => ({ })); const fieldFormats = fieldFormatsMock; - let object: any = {}; function setDocsourcePayload(id: string | null, providedPayload: any) { @@ -43,16 +42,18 @@ describe('IndexPatterns', () => { let savedObjectsClient: SavedObjectsClientCommon; beforeEach(() => { + const indexPatternObj = { id: 'id', version: 'a', attributes: { title: 'title' } }; savedObjectsClient = {} as SavedObjectsClientCommon; savedObjectsClient.find = jest.fn( - () => - Promise.resolve([{ id: 'id', attributes: { title: 'title' } }]) as Promise< - Array> - > + () => Promise.resolve([indexPatternObj]) as Promise>> ); savedObjectsClient.delete = jest.fn(() => Promise.resolve({}) as Promise); - savedObjectsClient.get = jest.fn().mockImplementation(() => object); savedObjectsClient.create = jest.fn(); + savedObjectsClient.get = jest.fn().mockImplementation(async (type, id) => ({ + id: object.id, + version: object.version, + attributes: object.attributes, + })); savedObjectsClient.update = jest .fn() .mockImplementation(async (type, id, body, { version }) => { @@ -141,30 +142,73 @@ describe('IndexPatterns', () => { }); // Create a normal index patterns - const pattern = await indexPatterns.make('foo'); + const pattern = await indexPatterns.get('foo'); expect(pattern.version).toBe('fooa'); + indexPatterns.clearCache(); // Create the same one - we're going to handle concurrency - const samePattern = await indexPatterns.make('foo'); + const samePattern = await indexPatterns.get('foo'); expect(samePattern.version).toBe('fooaa'); // This will conflict because samePattern did a save (from refreshFields) // but the resave should work fine pattern.title = 'foo2'; - await indexPatterns.save(pattern); + await indexPatterns.updateSavedObject(pattern); // This should not be able to recover samePattern.title = 'foo3'; let result; try { - await indexPatterns.save(samePattern); + await indexPatterns.updateSavedObject(samePattern); } catch (err) { result = err; } expect(result.res.status).toBe(409); }); + + test('create', async () => { + const title = 'kibana-*'; + indexPatterns.refreshFields = jest.fn(); + + const indexPattern = await indexPatterns.create({ title }, true); + expect(indexPattern).toBeInstanceOf(IndexPattern); + expect(indexPattern.title).toBe(title); + expect(indexPatterns.refreshFields).not.toBeCalled(); + + await indexPatterns.create({ title }); + expect(indexPatterns.refreshFields).toBeCalled(); + }); + + test('createAndSave', async () => { + const title = 'kibana-*'; + indexPatterns.createSavedObject = jest.fn(); + indexPatterns.setDefault = jest.fn(); + await indexPatterns.createAndSave({ title }); + expect(indexPatterns.createSavedObject).toBeCalled(); + expect(indexPatterns.setDefault).toBeCalled(); + }); + + test('savedObjectToSpec', () => { + const savedObject = { + id: 'id', + version: 'version', + attributes: { + title: 'kibana-*', + timeFieldName: '@timestamp', + fields: '[]', + sourceFilters: '[{"value":"item1"},{"value":"item2"}]', + fieldFormatMap: '{"field":{}}', + typeMeta: '{}', + type: '', + }, + type: 'index-pattern', + references: [], + }; + + expect(indexPatterns.savedObjectToSpec(savedObject)).toMatchSnapshot(); + }); }); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 47484f8ec75bb..c56954ba6a29b 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -33,9 +33,17 @@ import { IIndexPatternsApiClient, GetFieldsOptions, IndexPatternSpec, + IndexPatternAttributes, + FieldSpec, + FieldFormatMap, + IndexPatternFieldMap, } from '../types'; import { FieldFormatsStartCommon } from '../../field_formats'; import { UI_SETTINGS, SavedObject } from '../../../common'; +import { SavedObjectNotFound } from '../../../../kibana_utils/common'; +import { IndexPatternMissingIndices } from '../lib'; +import { findByTitle } from '../utils'; +import { DuplicateIndexPatternError } from '../errors'; const indexPatternCache = createIndexPatternCache(); const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; @@ -86,6 +94,9 @@ export class IndexPatternsService { ); } + /** + * Refresh cache of index pattern ids and titles + */ private async refreshSavedObjectsCache() { this.savedObjectsCache = await this.savedObjectsClient.find({ type: 'index-pattern', @@ -94,6 +105,10 @@ export class IndexPatternsService { }); } + /** + * Get list of index pattern ids + * @param refresh Force refresh of index pattern list + */ getIds = async (refresh: boolean = false) => { if (!this.savedObjectsCache || refresh) { await this.refreshSavedObjectsCache(); @@ -104,6 +119,10 @@ export class IndexPatternsService { return this.savedObjectsCache.map((obj) => obj?.id); }; + /** + * Get list of index pattern titles + * @param refresh Force refresh of index pattern list + */ getTitles = async (refresh: boolean = false): Promise => { if (!this.savedObjectsCache || refresh) { await this.refreshSavedObjectsCache(); @@ -114,14 +133,10 @@ export class IndexPatternsService { return this.savedObjectsCache.map((obj) => obj?.attributes?.title); }; - getFieldsForTimePattern = (options: GetFieldsOptions = {}) => { - return this.apiClient.getFieldsForTimePattern(options); - }; - - getFieldsForWildcard = (options: GetFieldsOptions = {}) => { - return this.apiClient.getFieldsForWildcard(options); - }; - + /** + * Clear index pattern list cache + * @param id optionally clear a single id + */ clearCache = (id?: string) => { this.savedObjectsCache = null; if (id) { @@ -130,6 +145,7 @@ export class IndexPatternsService { indexPatternCache.clearAll(); } }; + getCache = async () => { if (!this.savedObjectsCache) { await this.refreshSavedObjectsCache(); @@ -137,6 +153,9 @@ export class IndexPatternsService { return this.savedObjectsCache; }; + /** + * Get default index pattern + */ getDefault = async () => { const defaultIndexPatternId = await this.config.get('defaultIndex'); if (defaultIndexPatternId) { @@ -146,47 +165,350 @@ export class IndexPatternsService { return null; }; + /** + * Optionally set default index pattern, unless force = true + * @param id + * @param force + */ + setDefault = async (id: string, force = false) => { + if (force || !this.config.get('defaultIndex')) { + await this.config.set('defaultIndex', id); + } + }; + + private isFieldRefreshRequired(specs?: IndexPatternFieldMap): boolean { + if (!specs) { + return true; + } + + return Object.values(specs).every((spec) => { + // See https://github.com/elastic/kibana/pull/8421 + const hasFieldCaps = 'aggregatable' in spec && 'searchable' in spec; + + // See https://github.com/elastic/kibana/pull/11969 + const hasDocValuesFlag = 'readFromDocValues' in spec; + + return !hasFieldCaps || !hasDocValuesFlag; + }); + } + + /** + * Get field list by providing { pattern } + * @param options + */ + getFieldsForWildcard = async (options: GetFieldsOptions = {}) => { + const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); + return this.apiClient.getFieldsForWildcard({ + pattern: options.pattern, + metaFields, + type: options.type, + params: options.params || {}, + }); + }; + + /** + * Get field list by providing an index patttern (or spec) + * @param options + */ + getFieldsForIndexPattern = async ( + indexPattern: IndexPattern | IndexPatternSpec, + options: GetFieldsOptions = {} + ) => + this.getFieldsForWildcard({ + pattern: indexPattern.title as string, + ...options, + type: indexPattern.type, + params: indexPattern.typeMeta && indexPattern.typeMeta.params, + }); + + /** + * Refresh field list for a given index pattern + * @param indexPattern + */ + refreshFields = async (indexPattern: IndexPattern) => { + try { + const fields = await this.getFieldsForIndexPattern(indexPattern); + const scripted = indexPattern.getScriptedFields().map((field) => field.spec); + indexPattern.fields.replaceAll([...fields, ...scripted]); + } catch (err) { + if (err instanceof IndexPatternMissingIndices) { + this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' }); + } + + this.onError(err, { + title: i18n.translate('data.indexPatterns.fetchFieldErrorTitle', { + defaultMessage: 'Error fetching fields for index pattern {title} (ID: {id})', + values: { id: indexPattern.id, title: indexPattern.title }, + }), + }); + } + }; + + /** + * Refreshes a field list from a spec before an index pattern instance is created + * @param fields + * @param id + * @param title + * @param options + */ + private refreshFieldSpecMap = async ( + fields: IndexPatternFieldMap, + id: string, + title: string, + options: GetFieldsOptions + ) => { + const scriptdFields = Object.values(fields).filter((field) => field.scripted); + try { + const newFields = await this.getFieldsForWildcard(options); + return this.fieldArrayToMap([...newFields, ...scriptdFields]); + } catch (err) { + if (err instanceof IndexPatternMissingIndices) { + this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' }); + return {}; + } + + this.onError(err, { + title: i18n.translate('data.indexPatterns.fetchFieldErrorTitle', { + defaultMessage: 'Error fetching fields for index pattern {title} (ID: {id})', + values: { id, title }, + }), + }); + } + return fields; + }; + + /** + * Applies a set of formats to a set of fields + * @param fieldSpecs + * @param fieldFormatMap + */ + private addFormatsToFields = (fieldSpecs: FieldSpec[], fieldFormatMap: FieldFormatMap) => { + Object.entries(fieldFormatMap).forEach(([fieldName, value]) => { + const field = fieldSpecs.find((fld: FieldSpec) => fld.name === fieldName); + if (field) { + field.format = value; + } + }); + }; + + /** + * Converts field array to map + * @param fields + */ + fieldArrayToMap = (fields: FieldSpec[]) => + fields.reduce((collector, field) => { + collector[field.name] = field; + return collector; + }, {}); + + /** + * Converts index pattern saved object to index pattern spec + * @param savedObject + */ + + savedObjectToSpec = (savedObject: SavedObject): IndexPatternSpec => { + const { + id, + version, + attributes: { + title, + timeFieldName, + intervalName, + fields, + sourceFilters, + fieldFormatMap, + typeMeta, + type, + }, + } = savedObject; + + const parsedSourceFilters = sourceFilters ? JSON.parse(sourceFilters) : undefined; + const parsedTypeMeta = typeMeta ? JSON.parse(typeMeta) : undefined; + const parsedFieldFormatMap = fieldFormatMap ? JSON.parse(fieldFormatMap) : {}; + const parsedFields: FieldSpec[] = fields ? JSON.parse(fields) : []; + + this.addFormatsToFields(parsedFields, parsedFieldFormatMap); + return { + id, + version, + title, + intervalName, + timeFieldName, + sourceFilters: parsedSourceFilters, + fields: this.fieldArrayToMap(parsedFields), + typeMeta: parsedTypeMeta, + type, + }; + }; + + /** + * Get an index pattern by id. Cache optimized + * @param id + */ + get = async (id: string): Promise => { const cache = indexPatternCache.get(id); if (cache) { return cache; } - const indexPattern = await this.make(id); + const savedObject = await this.savedObjectsClient.get( + savedObjectType, + id + ); + + if (!savedObject.version) { + throw new SavedObjectNotFound(savedObjectType, id, 'management/kibana/indexPatterns'); + } + + const spec = this.savedObjectToSpec(savedObject); + const { title, type, typeMeta } = spec; + const parsedFieldFormats: FieldFormatMap = savedObject.attributes.fieldFormatMap + ? JSON.parse(savedObject.attributes.fieldFormatMap) + : {}; + + const isFieldRefreshRequired = this.isFieldRefreshRequired(spec.fields); + let isSaveRequired = isFieldRefreshRequired; + try { + spec.fields = isFieldRefreshRequired + ? await this.refreshFieldSpecMap(spec.fields || {}, id, spec.title as string, { + pattern: title, + metaFields: await this.config.get(UI_SETTINGS.META_FIELDS), + type, + params: typeMeta && typeMeta.params, + }) + : spec.fields; + } catch (err) { + isSaveRequired = false; + if (err instanceof IndexPatternMissingIndices) { + this.onNotification({ + title: (err as any).message, + color: 'danger', + iconType: 'alert', + }); + } else { + this.onError(err, { + title: i18n.translate('data.indexPatterns.fetchFieldErrorTitle', { + defaultMessage: 'Error fetching fields for index pattern {title} (ID: {id})', + values: { id, title }, + }), + }); + } + } + + Object.entries(parsedFieldFormats).forEach(([fieldName, value]) => { + const field = spec.fields?.[fieldName]; + if (field) { + field.format = value; + } + }); + + const indexPattern = await this.create(spec, true); + indexPatternCache.set(id, indexPattern); + if (isSaveRequired) { + try { + this.updateSavedObject(indexPattern); + } catch (err) { + this.onError(err, { + title: i18n.translate('data.indexPatterns.fetchFieldSaveErrorTitle', { + defaultMessage: + 'Error saving after fetching fields for index pattern {title} (ID: {id})', + values: { + id: indexPattern.id, + title: indexPattern.title, + }, + }), + }); + } + } - return indexPatternCache.set(id, indexPattern); + indexPattern.resetOriginalSavedObjectBody(); + return indexPattern; }; - async specToIndexPattern(spec: IndexPatternSpec) { + /** + * Create a new index pattern instance + * @param spec + * @param skipFetchFields + */ + async create(spec: IndexPatternSpec, skipFetchFields = false): Promise { const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE); const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); - const indexPattern = new IndexPattern(spec.id, { + const indexPattern = new IndexPattern({ + spec, savedObjectsClient: this.savedObjectsClient, - apiClient: this.apiClient, - patternCache: indexPatternCache, fieldFormats: this.fieldFormats, - indexPatternsService: this, - onNotification: this.onNotification, - onError: this.onError, shortDotsEnable, metaFields, }); - indexPattern.initFromSpec(spec); + if (!skipFetchFields) { + await this.refreshFields(indexPattern); + } + + return indexPattern; + } + + /** + * Create a new index pattern and save it right away + * @param spec + * @param override Overwrite if existing index pattern exists + * @param skipFetchFields + */ + + async createAndSave(spec: IndexPatternSpec, override = false, skipFetchFields = false) { + const indexPattern = await this.create(spec, skipFetchFields); + await this.createSavedObject(indexPattern, override); + await this.setDefault(indexPattern.id as string); return indexPattern; } - async save(indexPattern: IndexPattern, saveAttempts: number = 0): Promise { + /** + * Save a new index pattern + * @param indexPattern + * @param override Overwrite if existing index pattern exists + */ + + async createSavedObject(indexPattern: IndexPattern, override = false) { + const dupe = await findByTitle(this.savedObjectsClient, indexPattern.title); + if (dupe) { + if (override) { + await this.delete(dupe.id); + } else { + throw new DuplicateIndexPatternError(`Duplicate index pattern: ${indexPattern.title}`); + } + } + + const body = indexPattern.getAsSavedObjectBody(); + const response = await this.savedObjectsClient.create(savedObjectType, body, { + id: indexPattern.id, + }); + indexPattern.id = response.id; + indexPatternCache.set(indexPattern.id, indexPattern); + return indexPattern; + } + + /** + * Save existing index pattern. Will attempt to merge differences if there are conflicts + * @param indexPattern + * @param saveAttempts + */ + + async updateSavedObject( + indexPattern: IndexPattern, + saveAttempts: number = 0 + ): Promise { if (!indexPattern.id) return; - const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE); - const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); - const body = indexPattern.prepBody(); + // get the list of attributes + const body = indexPattern.getAsSavedObjectBody(); + const originalBody = indexPattern.getOriginalSavedObjectBody(); + // get changed keys const originalChangedKeys: string[] = []; Object.entries(body).forEach(([key, value]) => { - if (value !== indexPattern.originalBody[key]) { + if (value !== (originalBody as any)[key]) { originalChangedKeys.push(key); } }); @@ -197,92 +519,60 @@ export class IndexPatternsService { indexPattern.id = resp.id; indexPattern.version = resp.version; }) - .catch((err) => { + .catch(async (err) => { if (err?.res?.status === 409 && saveAttempts++ < MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS) { - const samePattern = new IndexPattern(indexPattern.id, { - savedObjectsClient: this.savedObjectsClient, - apiClient: this.apiClient, - patternCache: indexPatternCache, - fieldFormats: this.fieldFormats, - indexPatternsService: this, - onNotification: this.onNotification, - onError: this.onError, - shortDotsEnable, - metaFields, + const samePattern = await this.get(indexPattern.id as string); + // What keys changed from now and what the server returned + const updatedBody = samePattern.getAsSavedObjectBody(); + + // Build a list of changed keys from the server response + // and ensure we ignore the key if the server response + // is the same as the original response (since that is expected + // if we made a change in that key) + + const serverChangedKeys: string[] = []; + Object.entries(updatedBody).forEach(([key, value]) => { + if (value !== (body as any)[key] && value !== (originalBody as any)[key]) { + serverChangedKeys.push(key); + } }); - return samePattern.init().then(() => { - // What keys changed from now and what the server returned - const updatedBody = samePattern.prepBody(); - - // Build a list of changed keys from the server response - // and ensure we ignore the key if the server response - // is the same as the original response (since that is expected - // if we made a change in that key) - - const serverChangedKeys: string[] = []; - Object.entries(updatedBody).forEach(([key, value]) => { - if (value !== (body as any)[key] && value !== indexPattern.originalBody[key]) { - serverChangedKeys.push(key); - } - }); - - let unresolvedCollision = false; - for (const originalKey of originalChangedKeys) { - for (const serverKey of serverChangedKeys) { - if (originalKey === serverKey) { - unresolvedCollision = true; - break; - } + let unresolvedCollision = false; + for (const originalKey of originalChangedKeys) { + for (const serverKey of serverChangedKeys) { + if (originalKey === serverKey) { + unresolvedCollision = true; + break; } } + } - if (unresolvedCollision) { - const title = i18n.translate('data.indexPatterns.unableWriteLabel', { - defaultMessage: - 'Unable to write index pattern! Refresh the page to get the most up to date changes for this index pattern.', - }); - - this.onNotification({ title, color: 'danger' }); - throw err; - } - - // Set the updated response on this object - serverChangedKeys.forEach((key) => { - (indexPattern as any)[key] = (samePattern as any)[key]; + if (unresolvedCollision) { + const title = i18n.translate('data.indexPatterns.unableWriteLabel', { + defaultMessage: + 'Unable to write index pattern! Refresh the page to get the most up to date changes for this index pattern.', }); - indexPattern.version = samePattern.version; - // Clear cache - indexPatternCache.clear(indexPattern.id!); + this.onNotification({ title, color: 'danger' }); + throw err; + } - // Try the save again - return this.save(indexPattern, saveAttempts); + // Set the updated response on this object + serverChangedKeys.forEach((key) => { + (indexPattern as any)[key] = (samePattern as any)[key]; }); + indexPattern.version = samePattern.version; + + // Clear cache + indexPatternCache.clear(indexPattern.id!); + + // Try the save again + return this.updateSavedObject(indexPattern, saveAttempts); } throw err; }); } - async make(id?: string): Promise { - const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE); - const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); - - const indexPattern = new IndexPattern(id, { - savedObjectsClient: this.savedObjectsClient, - apiClient: this.apiClient, - patternCache: indexPatternCache, - fieldFormats: this.fieldFormats, - indexPatternsService: this, - onNotification: this.onNotification, - onError: this.onError, - shortDotsEnable, - metaFields, - }); - - return indexPattern.init(); - } - /** * Deletes an index pattern from .kibana index * @param indexPatternId: Id of kibana Index Pattern to delete diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 2264c5e3aaf56..cb0c3aa0de38e 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -24,6 +24,8 @@ import { IFieldType } from './fields'; import { SerializedFieldFormat } from '../../../expressions/common'; import { KBN_FIELD_TYPES, IndexPatternField, FieldFormat } from '..'; +export type FieldFormatMap = Record; + export interface IIndexPattern { fields: IFieldType[]; title: string; @@ -31,22 +33,12 @@ export interface IIndexPattern { type?: string; timeFieldName?: string; getTimeField?(): IFieldType | undefined; + fieldFormatMap?: Record | undefined>; getFormatterForField?: ( field: IndexPatternField | IndexPatternField['spec'] | IFieldType ) => FieldFormat; - fieldFormatMap?: Record< - string, - { - id: string; - params: unknown; - } - >; } -/** - * Use data plugin interface instead - * @deprecated - */ export interface IndexPatternAttributes { type: string; fields: string; @@ -168,15 +160,18 @@ export interface FieldSpec { indexed?: boolean; } +export type IndexPatternFieldMap = Record; + export interface IndexPatternSpec { id?: string; version?: string; - - title: string; + title?: string; + intervalName?: string; timeFieldName?: string; sourceFilters?: SourceFilter[]; - fields?: FieldSpec[]; + fields?: IndexPatternFieldMap; typeMeta?: TypeMeta; + type?: string; } export interface SourceFilter { diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 5038af9409316..42c8864bb0bc0 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -230,6 +230,8 @@ import { formatHitProvider, } from './index_patterns'; +export type { IndexPatternsService } from './index_patterns'; + // Index patterns namespace: export const indexPatterns = { ILLEGAL_CHARACTERS_KEY, @@ -262,9 +264,12 @@ export { UI_SETTINGS, TypeMeta as IndexPatternTypeMeta, AggregationRestrictions as IndexPatternAggRestrictions, + IndexPatternSpec, fieldList, } from '../common'; +export { DuplicateIndexPatternError } from '../common/index_patterns/errors'; + /* * Autocomplete query suggestions: */ diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index b2f1cc19d0f90..91f7239401255 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -454,6 +454,13 @@ export interface DataPublicPluginStartUi { SearchBar: React.ComponentType; } +// Warning: (ae-missing-release-tag) "DuplicateIndexPatternError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class DuplicateIndexPatternError extends Error { + constructor(message: string); +} + // @public (undocumented) export enum ES_FIELD_TYPES { // (undocumented) @@ -1004,11 +1011,10 @@ export interface IFieldType { // // @public (undocumented) export interface IIndexPattern { + // Warning: (ae-forgotten-export) The symbol "SerializedFieldFormat" needs to be exported by the entry point index.d.ts + // // (undocumented) - fieldFormatMap?: Record; + fieldFormatMap?: Record | undefined>; // (undocumented) fields: IFieldType[]; // (undocumented) @@ -1043,10 +1049,12 @@ export interface IIndexPatternFieldList extends Array { removeAll(): void; // (undocumented) replaceAll(specs: FieldSpec[]): void; + // Warning: (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts + // // (undocumented) toSpec(options?: { getFormatterForField?: IndexPattern['getFormatterForField']; - }): FieldSpec[]; + }): IndexPatternFieldMap; // (undocumented) update(field: FieldSpec): void; } @@ -1079,27 +1087,23 @@ export type IMetricAggType = MetricAggType; // @public (undocumented) export class IndexPattern implements IIndexPattern { // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts - constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps); - // (undocumented) - addScriptedField(name: string, script: string, fieldType: string | undefined, lang: string): Promise; - // (undocumented) - create(allowOverride?: boolean): Promise; - // (undocumented) - _fetchFields(): Promise; + constructor({ spec, savedObjectsClient, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); + addScriptedField(name: string, script: string, fieldType?: string, lang?: string): Promise; // (undocumented) - fieldFormatMap: any; + fieldFormatMap: Record; // (undocumented) fields: IIndexPatternFieldList & { - toSpec: () => FieldSpec[]; + toSpec: () => IndexPatternFieldMap; }; // (undocumented) - fieldsFetcher: any; + flattenHit: (hit: Record, deep?: boolean) => Record; // (undocumented) - flattenHit: any; + formatField: FormatFieldFn; // (undocumented) - formatField: any; - // (undocumented) - formatHit: any; + formatHit: { + (hit: Record, type?: string): any; + formatField: FormatFieldFn; + }; // (undocumented) getAggregationRestrictions(): Record> | undefined; + getAsSavedObjectBody(): { + title: string; + timeFieldName: string | undefined; + intervalName: string | undefined; + sourceFilters: string | undefined; + fields: string | undefined; + fieldFormatMap: string | undefined; + type: string | undefined; + typeMeta: string | undefined; + }; // (undocumented) getComputedFields(): { storedFields: string[]; @@ -1120,13 +1134,21 @@ export class IndexPattern implements IIndexPattern { }; // (undocumented) getFieldByName(name: string): IndexPatternField | undefined; - // (undocumented) getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat; // (undocumented) getNonScriptedFields(): IndexPatternField[]; + getOriginalSavedObjectBody: () => { + title?: string | undefined; + timeFieldName?: string | undefined; + intervalName?: string | undefined; + fields?: string | undefined; + sourceFilters?: string | undefined; + fieldFormatMap?: string | undefined; + typeMeta?: string | undefined; + type?: string | undefined; + }; // (undocumented) getScriptedFields(): IndexPatternField[]; - // (undocumented) getSourceFiltering(): { excludes: any[]; }; @@ -1135,12 +1157,6 @@ export class IndexPattern implements IIndexPattern { // (undocumented) id?: string; // (undocumented) - init(): Promise; - // Warning: (ae-forgotten-export) The symbol "IndexPatternSpec" needs to be exported by the entry point index.d.ts - // - // (undocumented) - initFromSpec(spec: IndexPatternSpec): this; - // (undocumented) intervalName: string | undefined; // (undocumented) isTimeBased(): boolean; @@ -1149,30 +1165,11 @@ export class IndexPattern implements IIndexPattern { // (undocumented) isTimeNanosBased(): boolean; // (undocumented) - isWildcard(): boolean; - // (undocumented) metaFields: string[]; // (undocumented) - originalBody: { - [key: string]: any; - }; - // (undocumented) popularizeField(fieldName: string, unit?: number): Promise; - // (undocumented) - prepBody(): { - title: string; - timeFieldName: string | undefined; - intervalName: string | undefined; - sourceFilters: string | undefined; - fields: string | undefined; - fieldFormatMap: string | undefined; - type: string | undefined; - typeMeta: string | undefined; - }; - // (undocumented) - refreshFields(): Promise; - // (undocumented) removeScriptedField(fieldName: string): void; + resetOriginalSavedObjectBody: () => void; // Warning: (ae-forgotten-export) The symbol "SourceFilter" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1205,7 +1202,7 @@ export type IndexPatternAggRestrictions = Record | undefined; set conflictDescriptions(conflictDescriptions: Record | undefined); - // (undocumented) get count(): number; set count(count: number); // (undocumented) @@ -1244,14 +1239,12 @@ export class IndexPatternField implements IFieldType { get esTypes(): string[] | undefined; // (undocumented) get filterable(): boolean; - // (undocumented) get lang(): string | undefined; set lang(lang: string | undefined); // (undocumented) get name(): string; // (undocumented) get readFromDocValues(): boolean; - // (undocumented) get script(): string | undefined; set script(script: string | undefined); // (undocumented) @@ -1306,7 +1299,6 @@ export const indexPatterns: { formatHitProvider: typeof formatHitProvider; }; -// Warning: (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "IndexPatternsContract" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1339,6 +1331,63 @@ export class IndexPatternSelect extends Component { UNSAFE_componentWillReceiveProps(nextProps: IndexPatternSelectProps): void; } +// Warning: (ae-missing-release-tag) "IndexPatternSpec" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface IndexPatternSpec { + // (undocumented) + fields?: IndexPatternFieldMap; + // (undocumented) + id?: string; + // (undocumented) + intervalName?: string; + // (undocumented) + sourceFilters?: SourceFilter[]; + // (undocumented) + timeFieldName?: string; + // (undocumented) + title?: string; + // (undocumented) + type?: string; + // (undocumented) + typeMeta?: IndexPatternTypeMeta; + // (undocumented) + version?: string; +} + +// Warning: (ae-missing-release-tag) "IndexPatternsService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class IndexPatternsService { + // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceDeps" needs to be exported by the entry point index.d.ts + constructor({ uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, }: IndexPatternsServiceDeps); + clearCache: (id?: string | undefined) => void; + create(spec: IndexPatternSpec, skipFetchFields?: boolean): Promise; + createAndSave(spec: IndexPatternSpec, override?: boolean, skipFetchFields?: boolean): Promise; + createSavedObject(indexPattern: IndexPattern, override?: boolean): Promise; + delete(indexPatternId: string): Promise<{}>; + // Warning: (ae-forgotten-export) The symbol "EnsureDefaultIndexPattern" needs to be exported by the entry point index.d.ts + // + // (undocumented) + ensureDefaultIndexPattern: EnsureDefaultIndexPattern; + fieldArrayToMap: (fields: FieldSpec[]) => Record; + get: (id: string) => Promise; + // Warning: (ae-forgotten-export) The symbol "IndexPatternSavedObjectAttrs" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getCache: () => Promise[] | null | undefined>; + getDefault: () => Promise; + getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions) => Promise; + // Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts + getFieldsForWildcard: (options?: GetFieldsOptions) => Promise; + getIds: (refresh?: boolean) => Promise; + getTitles: (refresh?: boolean) => Promise; + refreshFields: (indexPattern: IndexPattern) => Promise; + savedObjectToSpec: (savedObject: SavedObject) => IndexPatternSpec; + setDefault: (id: string, force?: boolean) => Promise; + updateSavedObject(indexPattern: IndexPattern, saveAttempts?: number): Promise; +} + // Warning: (ae-missing-release-tag) "TypeMeta" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2172,6 +2221,7 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:70:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:98:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:67:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FilterLabel" needs to be exported by the entry point index.d.ts @@ -2200,27 +2250,27 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:380:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:380:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:380:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:380:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:383:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:392:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:393:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:394:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:404:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:397:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:404:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 9be8ef1b53423..43080cc5a5989 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -290,3 +290,5 @@ export const config: PluginConfigDescriptor = { }, schema: configSchema, }; + +export type { IndexPatternsService } from './index_patterns'; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 05b99c754edf7..6d4112543ce0e 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -36,6 +36,7 @@ import { ConfigDeprecationProvider } from '@kbn/config'; import { CoreSetup } from 'src/core/server'; import { CoreSetup as CoreSetup_2 } from 'kibana/server'; import { CoreStart } from 'src/core/server'; +import { CoreStart as CoreStart_2 } from 'kibana/server'; import { CountParams } from 'elasticsearch'; import { CreateDocumentParams } from 'elasticsearch'; import { DeleteDocumentByQueryParams } from 'elasticsearch'; @@ -126,6 +127,7 @@ import { PackageInfo } from '@kbn/config'; import { PathConfigType } from '@kbn/utils'; import { PingParams } from 'elasticsearch'; import { Plugin as Plugin_2 } from 'src/core/server'; +import { Plugin as Plugin_3 } from 'kibana/server'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/server'; import { PutScriptParams } from 'elasticsearch'; import { PutTemplateParams } from 'elasticsearch'; @@ -628,29 +630,25 @@ export type IMetricAggType = MetricAggType; // @public (undocumented) export class IndexPattern implements IIndexPattern { // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts - constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps); + constructor({ spec, savedObjectsClient, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); + addScriptedField(name: string, script: string, fieldType?: string, lang?: string): Promise; // (undocumented) - addScriptedField(name: string, script: string, fieldType: string | undefined, lang: string): Promise; - // (undocumented) - create(allowOverride?: boolean): Promise; - // (undocumented) - _fetchFields(): Promise; - // (undocumented) - fieldFormatMap: any; + fieldFormatMap: Record; // Warning: (ae-forgotten-export) The symbol "IIndexPatternFieldList" needs to be exported by the entry point index.d.ts // // (undocumented) fields: IIndexPatternFieldList & { - toSpec: () => FieldSpec[]; + toSpec: () => IndexPatternFieldMap; }; // (undocumented) - fieldsFetcher: any; - // (undocumented) - flattenHit: any; + flattenHit: (hit: Record, deep?: boolean) => Record; // (undocumented) - formatField: any; + formatField: FormatFieldFn; // (undocumented) - formatHit: any; + formatHit: { + (hit: Record, type?: string): any; + formatField: FormatFieldFn; + }; // (undocumented) getAggregationRestrictions(): Record> | undefined; + getAsSavedObjectBody(): { + title: string; + timeFieldName: string | undefined; + intervalName: string | undefined; + sourceFilters: string | undefined; + fields: string | undefined; + fieldFormatMap: string | undefined; + type: string | undefined; + typeMeta: string | undefined; + }; // (undocumented) getComputedFields(): { storedFields: string[]; @@ -671,15 +679,23 @@ export class IndexPattern implements IIndexPattern { }; // (undocumented) getFieldByName(name: string): IndexPatternField | undefined; - // (undocumented) getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat; // Warning: (ae-forgotten-export) The symbol "IndexPatternField" needs to be exported by the entry point index.d.ts // // (undocumented) getNonScriptedFields(): IndexPatternField[]; + getOriginalSavedObjectBody: () => { + title?: string | undefined; + timeFieldName?: string | undefined; + intervalName?: string | undefined; + fields?: string | undefined; + sourceFilters?: string | undefined; + fieldFormatMap?: string | undefined; + typeMeta?: string | undefined; + type?: string | undefined; + }; // (undocumented) getScriptedFields(): IndexPatternField[]; - // (undocumented) getSourceFiltering(): { excludes: any[]; }; @@ -688,12 +704,6 @@ export class IndexPattern implements IIndexPattern { // (undocumented) id?: string; // (undocumented) - init(): Promise; - // Warning: (ae-forgotten-export) The symbol "IndexPatternSpec" needs to be exported by the entry point index.d.ts - // - // (undocumented) - initFromSpec(spec: IndexPatternSpec): this; - // (undocumented) intervalName: string | undefined; // (undocumented) isTimeBased(): boolean; @@ -702,30 +712,11 @@ export class IndexPattern implements IIndexPattern { // (undocumented) isTimeNanosBased(): boolean; // (undocumented) - isWildcard(): boolean; - // (undocumented) metaFields: string[]; // (undocumented) - originalBody: { - [key: string]: any; - }; - // (undocumented) popularizeField(fieldName: string, unit?: number): Promise; - // (undocumented) - prepBody(): { - title: string; - timeFieldName: string | undefined; - intervalName: string | undefined; - sourceFilters: string | undefined; - fields: string | undefined; - fieldFormatMap: string | undefined; - type: string | undefined; - typeMeta: string | undefined; - }; - // (undocumented) - refreshFields(): Promise; - // (undocumented) removeScriptedField(fieldName: string): void; + resetOriginalSavedObjectBody: () => void; // Warning: (ae-forgotten-export) The symbol "SourceFilter" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -734,6 +725,8 @@ export class IndexPattern implements IIndexPattern { timeFieldName: string | undefined; // (undocumented) title: string; + // Warning: (ae-forgotten-export) The symbol "IndexPatternSpec" needs to be exported by the entry point index.d.ts + // // (undocumented) toSpec(): IndexPatternSpec; // (undocumented) @@ -748,7 +741,7 @@ export class IndexPattern implements IIndexPattern { // Warning: (ae-missing-release-tag) "IndexPatternAttributes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public @deprecated +// @public (undocumented) export interface IndexPatternAttributes { // (undocumented) fieldFormatMap?: string; @@ -796,6 +789,21 @@ export class IndexPatternsFetcher { }): Promise; } +// Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStart" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "IndexPatternsService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class IndexPatternsService implements Plugin_3 { + // (undocumented) + setup(core: CoreSetup_2): void; + // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStartDeps" needs to be exported by the entry point index.d.ts + // + // (undocumented) + start(core: CoreStart_2, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { + indexPatternsServiceFactory: (kibanaRequest: KibanaRequest) => Promise; + }; +} + // Warning: (ae-missing-release-tag) "ISearchOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -995,7 +1003,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; + indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; }; }; // (undocumented) @@ -1029,8 +1037,6 @@ export interface PluginStart { // // (undocumented) fieldFormats: FieldFormatsStart; - // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStart" needs to be exported by the entry point index.d.ts - // // (undocumented) indexPatterns: IndexPatternsServiceStart; // (undocumented) @@ -1216,6 +1222,8 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // // src/plugins/data/common/es_query/filters/meta_filter.ts:53:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:70:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts @@ -1250,6 +1258,7 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:245:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:249:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:252:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index_patterns/index_patterns_service.ts:50:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:88:66 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts index 8746883a5d968..dad208c815675 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts @@ -142,7 +142,7 @@ describe('fieldCalculator', function () { let hits: any; beforeEach(function () { - hits = _.each(_.cloneDeep(realHits), indexPattern.flattenHit); + hits = _.each(_.cloneDeep(realHits), (hit) => indexPattern.flattenHit(hit)); }); it('Should return an array of values for _source fields', function () { diff --git a/src/plugins/discover/public/application/components/table/table.test.tsx b/src/plugins/discover/public/application/components/table/table.test.tsx index 07e9e0a129a26..2874e2483275b 100644 --- a/src/plugins/discover/public/application/components/table/table.test.tsx +++ b/src/plugins/discover/public/application/components/table/table.test.tsx @@ -22,7 +22,7 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { DocViewTable } from './table'; import { indexPatterns, IndexPattern } from '../../../../../data/public'; -const indexPattern = { +const indexPattern = ({ fields: { getAll: () => [ { @@ -60,7 +60,7 @@ const indexPattern = { metaFields: ['_index', '_score'], flattenHit: undefined, formatHit: jest.fn((hit) => hit._source), -} as IndexPattern; +} as unknown) as IndexPattern; indexPattern.fields.getByName = (name: string) => { return indexPattern.fields.getAll().find((field) => field.name === name); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/__snapshots__/step_time_field.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/__snapshots__/step_time_field.test.tsx.snap index 6cc92d20cfdcc..544e3ba983122 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/__snapshots__/step_time_field.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/__snapshots__/step_time_field.test.tsx.snap @@ -27,7 +27,7 @@ exports[`StepTimeField should render "Custom index pattern ID already exists" wh /> ({ - fieldsFetcher: { - fetchForWildcard: jest.fn().mockReturnValue(Promise.resolve(fields)), - }, - }), + create: () => ({}), + getFieldsForWildcard: jest.fn().mockReturnValue(Promise.resolve(fields)), } as any; describe('StepTimeField', () => { diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx index 5d33a08557fed..cacabb6d7623b 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx @@ -108,12 +108,12 @@ export class StepTimeField extends Component { }); test('invokes the provided services when creating an index pattern', async () => { - const create = jest.fn().mockImplementation(() => 'id'); + const newIndexPatternAndSave = jest.fn().mockImplementation(async () => { + return indexPattern; + }); const clear = jest.fn(); mockContext.data.indexPatterns.clearCache = clear; const indexPattern = ({ @@ -151,11 +153,10 @@ describe('CreateIndexPatternWizard', () => { title: 'my-fake-index-pattern', timeFieldName: 'timestamp', fields: [], - create, + _fetchFields: jest.fn(), } as unknown) as IndexPattern; - mockContext.data.indexPatterns.make = async () => { - return indexPattern; - }; + mockContext.data.indexPatterns.createAndSave = newIndexPatternAndSave; + mockContext.data.indexPatterns.setDefault = jest.fn(); const component = createComponentWithContext( CreateIndexPatternWizard, @@ -165,9 +166,8 @@ describe('CreateIndexPatternWizard', () => { component.setState({ indexPattern: 'foo' }); await (component.instance() as CreateIndexPatternWizard).createIndexPattern(undefined, 'id'); - expect(mockContext.uiSettings.get).toBeCalled(); - expect(create).toBeCalled(); - expect(clear).toBeCalledWith('id'); - expect(routeComponentPropsMock.history.push).toBeCalledWith(`/patterns/id`); + expect(newIndexPatternAndSave).toBeCalled(); + expect(clear).toBeCalledWith('1'); + expect(routeComponentPropsMock.history.push).toBeCalledWith(`/patterns/1`); }); }); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx index a789ebbfadbce..aa97c21d766b9 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx @@ -40,6 +40,7 @@ import { ensureMinimumTime, getIndices } from './lib'; import { IndexPatternCreationConfig } from '../..'; import { IndexPatternManagmentContextValue } from '../../types'; import { MatchedItem } from './types'; +import { DuplicateIndexPatternError, IndexPattern } from '../../../../data/public'; interface CreateIndexPatternWizardState { step: number; @@ -156,50 +157,50 @@ export class CreateIndexPatternWizard extends Component< }; createIndexPattern = async (timeFieldName: string | undefined, indexPatternId: string) => { + let emptyPattern: IndexPattern; const { history } = this.props; const { indexPattern } = this.state; - const emptyPattern = await this.context.services.data.indexPatterns.make(); - - Object.assign(emptyPattern, { - id: indexPatternId, - title: indexPattern, - timeFieldName, - ...this.state.indexPatternCreationType.getIndexPatternMappings(), - }); - - const createdId = await emptyPattern.create(); - if (!createdId) { - const confirmMessage = i18n.translate( - 'indexPatternManagement.indexPattern.titleExistsLabel', - { - values: { title: emptyPattern.title }, - defaultMessage: "An index pattern with the title '{title}' already exists.", - } - ); - - const isConfirmed = await this.context.services.overlays.openConfirm(confirmMessage, { - confirmButtonText: i18n.translate( - 'indexPatternManagement.indexPattern.goToPatternButtonLabel', + try { + emptyPattern = await this.context.services.data.indexPatterns.createAndSave({ + id: indexPatternId, + title: indexPattern, + timeFieldName, + ...this.state.indexPatternCreationType.getIndexPatternMappings(), + }); + } catch (err) { + if (err instanceof DuplicateIndexPatternError) { + const confirmMessage = i18n.translate( + 'indexPatternManagement.indexPattern.titleExistsLabel', { - defaultMessage: 'Go to existing pattern', + values: { title: emptyPattern!.title }, + defaultMessage: "An index pattern with the title '{title}' already exists.", } - ), - }); + ); + + const isConfirmed = await this.context.services.overlays.openConfirm(confirmMessage, { + confirmButtonText: i18n.translate( + 'indexPatternManagement.indexPattern.goToPatternButtonLabel', + { + defaultMessage: 'Go to existing pattern', + } + ), + }); - if (isConfirmed) { - return history.push(`/patterns/${indexPatternId}`); + if (isConfirmed) { + return history.push(`/patterns/${indexPatternId}`); + } else { + return; + } } else { - return; + throw err; } } - if (!this.context.services.uiSettings.get('defaultIndex')) { - await this.context.services.uiSettings.set('defaultIndex', createdId); - } + await this.context.services.data.indexPatterns.setDefault(emptyPattern.id as string); - this.context.services.data.indexPatterns.clearCache(createdId); - history.push(`/patterns/${createdId}`); + this.context.services.data.indexPatterns.clearCache(emptyPattern.id as string); + history.push(`/patterns/${emptyPattern.id}`); }; goToTimeFieldStep = (indexPattern: string, selectedTimeField?: string) => { diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx index 13be9ca6c9c25..08edf42df60d8 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx @@ -96,7 +96,7 @@ export const CreateEditField = withRouter( indexPattern={indexPattern} spec={spec} services={{ - saveIndexPattern: data.indexPatterns.save.bind(data.indexPatterns), + saveIndexPattern: data.indexPatterns.updateSavedObject.bind(data.indexPatterns), redirectAway, }} /> diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index d09836019b0bc..67a20c428040f 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -121,7 +121,8 @@ export const EditIndexPattern = withRouter( const refreshFields = () => { overlays.openConfirm(confirmMessage, confirmModalOptionsRefresh).then(async (isConfirmed) => { if (isConfirmed) { - await indexPattern.refreshFields(); + await data.indexPatterns.refreshFields(indexPattern); + await data.indexPatterns.updateSavedObject(indexPattern); setFields(indexPattern.getNonScriptedFields()); } }); @@ -236,7 +237,7 @@ export const EditIndexPattern = withRouter( void; painlessDocLink: string; - saveIndexPattern: DataPublicPluginStart['indexPatterns']['save']; + saveIndexPattern: DataPublicPluginStart['indexPatterns']['updateSavedObject']; } interface ScriptedFieldsTableState { diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.tsx index b00648f124716..cd311db513c09 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.tsx @@ -30,7 +30,7 @@ export interface SourceFiltersTableProps { filterFilter: string; fieldWildcardMatcher: Function; onAddOrRemoveFilter?: Function; - saveIndexPattern: DataPublicPluginStart['indexPatterns']['save']; + saveIndexPattern: DataPublicPluginStart['indexPatterns']['updateSavedObject']; } export interface SourceFiltersTableState { diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx index 101399ef02b73..5c29dfafd3c07 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx @@ -49,7 +49,7 @@ import { getTabs, getPath, convertToEuiSelectOption } from './utils'; interface TabsProps extends Pick { indexPattern: IndexPattern; fields: IndexPatternField[]; - saveIndexPattern: DataPublicPluginStart['indexPatterns']['save']; + saveIndexPattern: DataPublicPluginStart['indexPatterns']['updateSavedObject']; } const searchAriaLabel = i18n.translate( diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx index 2b484d1d837bf..4fae91e78f8f9 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx @@ -133,7 +133,7 @@ export interface FieldEdiorProps { spec: IndexPatternField['spec']; services: { redirectAway: () => void; - saveIndexPattern: DataPublicPluginStart['indexPatterns']['save']; + saveIndexPattern: DataPublicPluginStart['indexPatterns']['updateSavedObject']; }; } @@ -825,7 +825,7 @@ export class FieldEditor extends PureComponent { + .catch(() => { if (oldField) { indexPattern.fields.update(oldField); } else { diff --git a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts index 679ea5ffc23ee..eb95c213e680d 100644 --- a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts +++ b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts @@ -24,10 +24,11 @@ import { SavedObject, SavedObjectLoader } from '../../../saved_objects/public'; import { DataPublicPluginStart, IndexPatternsContract, - IIndexPattern, injectSearchSourceReferences, + IndexPatternSpec, } from '../../../data/public'; import { FailedImport } from './process_import_response'; +import { DuplicateIndexPatternError, IndexPattern } from '../../../data/public'; type SavedObjectsRawDoc = Record; @@ -70,11 +71,10 @@ function addJsonFieldToIndexPattern( async function importIndexPattern( doc: SavedObjectsRawDoc, indexPatterns: IndexPatternsContract, - overwriteAll: boolean, + overwriteAll: boolean = false, openConfirm: OverlayStart['openConfirm'] ) { // TODO: consolidate this is the code in create_index_pattern_wizard.js - const emptyPattern = await indexPatterns.make(); const { title, timeFieldName, @@ -84,50 +84,53 @@ async function importIndexPattern( type, typeMeta, } = doc._source; - const importedIndexPattern = { + const indexPatternSpec: IndexPatternSpec = { id: doc._id, title, timeFieldName, - } as IIndexPattern; + }; + let emptyPattern: IndexPattern; if (type) { - importedIndexPattern.type = type; + indexPatternSpec.type = type; } - addJsonFieldToIndexPattern(importedIndexPattern, fields, 'fields', title); - addJsonFieldToIndexPattern(importedIndexPattern, fieldFormatMap, 'fieldFormatMap', title); - addJsonFieldToIndexPattern(importedIndexPattern, sourceFilters, 'sourceFilters', title); - addJsonFieldToIndexPattern(importedIndexPattern, typeMeta, 'typeMeta', title); - Object.assign(emptyPattern, importedIndexPattern); - - let newId = await emptyPattern.create(overwriteAll); - if (!newId) { - // We can override and we want to prompt for confirmation - const isConfirmed = await openConfirm( - i18n.translate('savedObjectsManagement.indexPattern.confirmOverwriteLabel', { - values: { title }, - defaultMessage: "Are you sure you want to overwrite '{title}'?", - }), - { - title: i18n.translate('savedObjectsManagement.indexPattern.confirmOverwriteTitle', { - defaultMessage: 'Overwrite {type}?', - values: { type }, + addJsonFieldToIndexPattern(indexPatternSpec, fields, 'fields', title); + addJsonFieldToIndexPattern(indexPatternSpec, fieldFormatMap, 'fieldFormatMap', title); + addJsonFieldToIndexPattern(indexPatternSpec, sourceFilters, 'sourceFilters', title); + addJsonFieldToIndexPattern(indexPatternSpec, typeMeta, 'typeMeta', title); + try { + emptyPattern = await indexPatterns.createAndSave(indexPatternSpec, overwriteAll, true); + } catch (err) { + if (err instanceof DuplicateIndexPatternError) { + // We can override and we want to prompt for confirmation + const isConfirmed = await openConfirm( + i18n.translate('savedObjectsManagement.indexPattern.confirmOverwriteLabel', { + values: { title }, + defaultMessage: "Are you sure you want to overwrite '{title}'?", }), - confirmButtonText: i18n.translate( - 'savedObjectsManagement.indexPattern.confirmOverwriteButton', - { - defaultMessage: 'Overwrite', - } - ), - } - ); + { + title: i18n.translate('savedObjectsManagement.indexPattern.confirmOverwriteTitle', { + defaultMessage: 'Overwrite {type}?', + values: { type }, + }), + confirmButtonText: i18n.translate( + 'savedObjectsManagement.indexPattern.confirmOverwriteButton', + { + defaultMessage: 'Overwrite', + } + ), + } + ); - if (isConfirmed) { - newId = (await emptyPattern.create(true)) as string; - } else { - return; + if (isConfirmed) { + emptyPattern = await indexPatterns.createAndSave(indexPatternSpec, true, true); + } else { + return; + } } } - indexPatterns.clearCache(newId); - return newId; + + indexPatterns.clearCache(emptyPattern!.id); + return emptyPattern!.id; } async function importDocument(obj: SavedObject, doc: SavedObjectsRawDoc, overwriteAll: boolean) { diff --git a/test/functional/apps/management/_index_pattern_popularity.js b/test/functional/apps/management/_index_pattern_popularity.js index e2fcf50ef2c12..530b8e1111a0c 100644 --- a/test/functional/apps/management/_index_pattern_popularity.js +++ b/test/functional/apps/management/_index_pattern_popularity.js @@ -60,7 +60,7 @@ export default function ({ getService, getPageObjects }) { // check that it is 0 (previous increase was cancelled const popularity = await PageObjects.settings.getPopularity(); log.debug('popularity = ' + popularity); - expect(popularity).to.be(''); + expect(popularity).to.be('0'); }); it('can be saved', async function () { diff --git a/test/plugin_functional/plugins/index_patterns/server/plugin.ts b/test/plugin_functional/plugins/index_patterns/server/plugin.ts index 1c85f226623cb..ddf9acb259983 100644 --- a/test/plugin_functional/plugins/index_patterns/server/plugin.ts +++ b/test/plugin_functional/plugins/index_patterns/server/plugin.ts @@ -78,7 +78,7 @@ export class IndexPatternsTestPlugin const id = (req.params as Record).id; const service = await data.indexPatterns.indexPatternsServiceFactory(req); const ip = await service.get(id); - await service.save(ip); + await service.updateSavedObject(ip); return res.ok(); } ); diff --git a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts index 2db9eb733f805..7e736ea7a066f 100644 --- a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts +++ b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts @@ -46,14 +46,14 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const body = await ( await supertest.get(`/api/index-patterns-plugin/get/${indexPatternId}`).expect(200) ).body; - expect(body.fields.length > 0).to.equal(true); + expect(typeof body.id).to.equal('string'); }); it('can update index pattern', async () => { - const body = await ( - await supertest.get(`/api/index-patterns-plugin/update/${indexPatternId}`).expect(200) - ).body; - expect(body).to.eql({}); + const resp = await supertest + .get(`/api/index-patterns-plugin/update/${indexPatternId}`) + .expect(200); + expect(resp.body).to.eql({}); }); it('can delete index pattern', async () => { diff --git a/x-pack/plugins/file_upload/public/util/indexing_service.js b/x-pack/plugins/file_upload/public/util/indexing_service.js index eb22b0228b48a..28cdb602455b5 100644 --- a/x-pack/plugins/file_upload/public/util/indexing_service.js +++ b/x-pack/plugins/file_upload/public/util/indexing_service.js @@ -189,19 +189,16 @@ async function chunkDataAndWriteToIndex({ id, index, data, mappings, settings }) } export async function createIndexPattern(indexPatternName) { - const indexPatterns = await indexPatternService.get(); try { - Object.assign(indexPatterns, { - id: '', - title: indexPatternName, - }); - - await indexPatterns.create(true); - const id = await getIndexPatternId(indexPatternName); - const indexPattern = await indexPatternService.get(id); + const indexPattern = await indexPatternService.createAndSave( + { + title: indexPatternName, + }, + true + ); return { success: true, - id, + id: indexPattern.id, fields: indexPattern.fields, }; } catch (error) { @@ -212,18 +209,6 @@ export async function createIndexPattern(indexPatternName) { } } -async function getIndexPatternId(name) { - const savedObjectSearch = await savedObjectsClient.find({ type: 'index-pattern', perPage: 1000 }); - const indexPatternSavedObjects = savedObjectSearch.savedObjects; - - if (indexPatternSavedObjects) { - const ip = indexPatternSavedObjects.find((i) => i.attributes.title === name); - return ip !== undefined ? ip.id : undefined; - } else { - return undefined; - } -} - export const getExistingIndexNames = async () => { const indexes = await httpService({ url: `/api/index_management/indices`, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 161dde51df43e..1c8bfafeb10ff 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -12,6 +12,7 @@ import { extractErrorMessage } from '../../../../../../../common/util/errors'; import { DeepReadonly } from '../../../../../../../common/types/common'; import { ml } from '../../../../../services/ml_api_service'; import { useMlContext } from '../../../../../contexts/ml'; +import { DuplicateIndexPatternError } from '../../../../../../../../../../src/plugins/data/public'; import { useRefreshAnalyticsList, @@ -130,19 +131,25 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { const indexPatternName = destinationIndex; try { - const newIndexPattern = await mlContext.indexPatterns.make(); + await mlContext.indexPatterns.createAndSave( + { + title: indexPatternName, + }, + false, + true + ); - Object.assign(newIndexPattern, { - id: '', - title: indexPatternName, + addRequestMessage({ + message: i18n.translate( + 'xpack.ml.dataframe.analytics.create.createIndexPatternSuccessMessage', + { + defaultMessage: 'Kibana index pattern {indexPatternName} created.', + values: { indexPatternName }, + } + ), }); - - const id = await newIndexPattern.create(); - - await mlContext.indexPatterns.clearCache(); - - // id returns false if there's a duplicate index pattern. - if (id === false) { + } catch (e) { + if (e instanceof DuplicateIndexPatternError) { addRequestMessage({ error: i18n.translate( 'xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError', @@ -158,34 +165,17 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { } ), }); - return; - } - - // check if there's a default index pattern, if not, - // set the newly created one as the default index pattern. - if (!mlContext.kibanaConfig.get('defaultIndex')) { - await mlContext.kibanaConfig.set('defaultIndex', id); + } else { + addRequestMessage({ + error: extractErrorMessage(e), + message: i18n.translate( + 'xpack.ml.dataframe.analytics.create.createIndexPatternErrorMessage', + { + defaultMessage: 'An error occurred creating the Kibana index pattern:', + } + ), + }); } - - addRequestMessage({ - message: i18n.translate( - 'xpack.ml.dataframe.analytics.create.createIndexPatternSuccessMessage', - { - defaultMessage: 'Kibana index pattern {indexPatternName} created.', - values: { indexPatternName }, - } - ), - }); - } catch (e) { - addRequestMessage({ - error: extractErrorMessage(e), - message: i18n.translate( - 'xpack.ml.dataframe.analytics.create.createIndexPatternErrorMessage', - { - defaultMessage: 'An error occurred creating the Kibana index pattern:', - } - ), - }); } }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js index 08b61a5fa4eed..2ad0c9b1ac263 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js @@ -215,7 +215,7 @@ export class ImportView extends Component { // mappings, use this field as the time field. // This relies on the field being populated by // the ingest pipeline on ingest - if (mappings[DEFAULT_TIME_FIELD] !== undefined) { + if (mappings.properties[DEFAULT_TIME_FIELD] !== undefined) { timeFieldName = DEFAULT_TIME_FIELD; this.setState({ timeFieldName }); } @@ -615,34 +615,16 @@ export class ImportView extends Component { } } -async function createKibanaIndexPattern( - indexPatternName, - indexPatterns, - timeFieldName, - kibanaConfig -) { +async function createKibanaIndexPattern(indexPatternName, indexPatterns, timeFieldName) { try { - const emptyPattern = await indexPatterns.make(); - - Object.assign(emptyPattern, { - id: '', + const emptyPattern = await indexPatterns.createAndSave({ title: indexPatternName, timeFieldName, }); - const id = await emptyPattern.create(); - - await indexPatterns.clearCache(); - - // check if there's a default index pattern, if not, - // set the newly created one as the default index pattern. - if (!kibanaConfig.get('defaultIndex')) { - await kibanaConfig.set('defaultIndex', id); - } - return { success: true, - id, + id: emptyPattern.id, }; } catch (error) { return { diff --git a/x-pack/plugins/ml/public/application/util/index_utils.ts b/x-pack/plugins/ml/public/application/util/index_utils.ts index 192552b25d15a..42be3dd8252f9 100644 --- a/x-pack/plugins/ml/public/application/util/index_utils.ts +++ b/x-pack/plugins/ml/public/application/util/index_utils.ts @@ -104,7 +104,11 @@ export function getQueryFromSavedSearch(savedSearch: SavedSearchSavedObject) { export function getIndexPatternById(id: string): Promise { if (indexPatternsContract !== null) { - return indexPatternsContract.get(id); + if (id) { + return indexPatternsContract.get(id); + } else { + return indexPatternsContract.create({}); + } } else { throw new Error('Index patterns are not initialized!'); } diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index fa601bbaed91e..418dcf78eda5c 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -43,6 +43,7 @@ import { useApi } from '../../../../hooks/use_api'; import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { RedirectToTransformManagement } from '../../../../common/navigation'; import { ToastNotificationText } from '../../../../components'; +import { DuplicateIndexPatternError } from '../../../../../../../../../src/plugins/data/public'; export interface StepDetailsExposedState { created: boolean; @@ -83,7 +84,6 @@ export const StepCreateForm: FC = React.memo( const deps = useAppDependencies(); const indexPatterns = deps.data.indexPatterns; - const uiSettings = deps.uiSettings; const toastNotifications = useToastNotifications(); useEffect(() => { @@ -189,35 +189,14 @@ export const StepCreateForm: FC = React.memo( const indexPatternName = transformConfig.dest.index; try { - const newIndexPattern = await indexPatterns.make(); - - Object.assign(newIndexPattern, { - id: '', - title: indexPatternName, - timeFieldName, - }); - const id = await newIndexPattern.create(); - - await indexPatterns.clearCache(); - - // id returns false if there's a duplicate index pattern. - if (id === false) { - toastNotifications.addDanger( - i18n.translate('xpack.transform.stepCreateForm.duplicateIndexPatternErrorMessage', { - defaultMessage: - 'An error occurred creating the Kibana index pattern {indexPatternName}: The index pattern already exists.', - values: { indexPatternName }, - }) - ); - setLoading(false); - return; - } - - // check if there's a default index pattern, if not, - // set the newly created one as the default index pattern. - if (!uiSettings.get('defaultIndex')) { - await uiSettings.set('defaultIndex', id); - } + const newIndexPattern = await indexPatterns.createAndSave( + { + title: indexPatternName, + timeFieldName, + }, + false, + true + ); toastNotifications.addSuccess( i18n.translate('xpack.transform.stepCreateForm.createIndexPatternSuccessMessage', { @@ -226,22 +205,32 @@ export const StepCreateForm: FC = React.memo( }) ); - setIndexPatternId(id); + setIndexPatternId(newIndexPattern.id); setLoading(false); return true; } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.stepCreateForm.createIndexPatternErrorMessage', { - defaultMessage: - 'An error occurred creating the Kibana index pattern {indexPatternName}:', - values: { indexPatternName }, - }), - text: toMountPoint( - - ), - }); - setLoading(false); - return false; + if (e instanceof DuplicateIndexPatternError) { + toastNotifications.addDanger( + i18n.translate('xpack.transform.stepCreateForm.duplicateIndexPatternErrorMessage', { + defaultMessage: + 'An error occurred creating the Kibana index pattern {indexPatternName}: The index pattern already exists.', + values: { indexPatternName }, + }) + ); + } else { + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.stepCreateForm.createIndexPatternErrorMessage', { + defaultMessage: + 'An error occurred creating the Kibana index pattern {indexPatternName}:', + values: { indexPatternName }, + }), + text: toMountPoint( + + ), + }); + setLoading(false); + return false; + } } }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index afcbf89ae80ad..890ba48eccf1f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -801,8 +801,6 @@ "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。", "data.indexPatterns.fetchFieldErrorTitle": "インデックスパターンのフィールド取得中にエラーが発生 {title} (ID: {id})", "data.indexPatterns.unableWriteLabel": "インデックスパターンを書き込めません!このインデックスパターンへの最新の変更を取得するには、ページを更新してください。", - "data.indexPatterns.unknownFieldErrorMessage": "インデックスパターン「{title}」のフィールド「{name}」が不明なフィールドタイプを使用しています。", - "data.indexPatterns.unknownFieldHeader": "不明なフィールドタイプ {type}", "data.noDataPopover.content": "この時間範囲にはデータが含まれていません表示するフィールドを増やし、グラフを作成するには、時間範囲を広げるか、調整してください。", "data.noDataPopover.dismissAction": "今後表示しない", "data.noDataPopover.subtitle": "ヒント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e5dfbe60eb88a..53e0f49d7877e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -801,8 +801,6 @@ "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "若要在 Kibana 中可视化和浏览数据,您需要创建索引模式,以从 Elasticsearch 检索数据。", "data.indexPatterns.fetchFieldErrorTitle": "提取索引模式 {title} (ID: {id}) 的字段时出错", "data.indexPatterns.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。", - "data.indexPatterns.unknownFieldErrorMessage": "indexPattern “{title}” 中的字段 “{name}” 使用未知字段类型。", - "data.indexPatterns.unknownFieldHeader": "未知字段类型 {type}", "data.noDataPopover.content": "此时间范围不包含任何数据。增大或调整时间范围,以查看更多的字段并创建图表。", "data.noDataPopover.dismissAction": "不再显示", "data.noDataPopover.subtitle": "提示", From d79fbb3f5c91dd46f336d47b8ccda62c5e59e4cd Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 22 Sep 2020 19:18:50 -0600 Subject: [PATCH 28/92] [Security Solution][Detection Engine] Bubbles up more error messages from ES queries to the UI (#78004) ## Summary Fixes: https://github.com/elastic/kibana/issues/77254 Bubbles up error messages from ES queries that have _shards.failures in them. For example if you have errors in your exceptions list you will need to see them bubbled up. Steps to reproduce: Go to a detections rule and add an invalid value within the exceptions such as this one below: Screen Shot 2020-09-21 at 7 52 59 AM Notice that rsa.internal.level value is not a numeric but a text string. You should now see this error message where before you could not: Screen Shot 2020-09-21 at 7 52 44 AM ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../signals/__mocks__/es_results.ts | 4 +- .../signals/find_ml_signals.ts | 8 +- .../signals/find_threshold_signals.ts | 1 + .../signals/search_after_bulk_create.ts | 149 ++++----- .../signals/signal_rule_alert_type.test.ts | 26 +- .../signals/signal_rule_alert_type.ts | 58 ++-- .../signals/single_search_after.test.ts | 74 ++++- .../signals/single_search_after.ts | 12 +- .../threat_mapping/create_threat_signal.ts | 6 +- .../threat_mapping/create_threat_signals.ts | 2 +- .../signals/threat_mapping/types.ts | 2 +- .../signals/threat_mapping/utils.test.ts | 2 +- .../signals/threat_mapping/utils.ts | 2 +- .../lib/detection_engine/signals/types.ts | 50 ++- .../detection_engine/signals/utils.test.ts | 286 +++++++++++++++++- .../lib/detection_engine/signals/utils.ts | 103 ++++++- .../security_solution/server/lib/types.ts | 17 ++ 17 files changed, 647 insertions(+), 155 deletions(-) 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 bbdb8ea0a36ed..9ee8c5cf298a1 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 @@ -337,7 +337,7 @@ export const repeatedSearchResultsWithSortId = ( guids: string[], ips?: string[], destIps?: string[] -) => ({ +): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -364,7 +364,7 @@ export const repeatedSearchResultsWithNoSortId = ( pageSize: number, guids: string[], ips?: string[] -) => ({ +): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts index bd9bf50688b58..89e3d28f451e4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts @@ -8,7 +8,7 @@ import dateMath from '@elastic/datemath'; import { KibanaRequest } from '../../../../../../../src/core/server'; import { MlPluginSetup } from '../../../../../ml/server'; -import { getAnomalies } from '../../machine_learning'; +import { AnomalyResults, getAnomalies } from '../../machine_learning'; export const findMlSignals = async ({ ml, @@ -24,7 +24,7 @@ export const findMlSignals = async ({ anomalyThreshold: number; from: string; to: string; -}) => { +}): Promise => { const { mlAnomalySearch } = ml.mlSystemProvider(request); const params = { jobIds: [jobId], @@ -32,7 +32,5 @@ export const findMlSignals = async ({ earliestMs: dateMath.parse(from)?.valueOf() ?? 0, latestMs: dateMath.parse(to)?.valueOf() ?? 0, }; - const relevantAnomalies = await getAnomalies(params, mlAnomalySearch); - - return relevantAnomalies; + return getAnomalies(params, mlAnomalySearch); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts index 251c043adb58b..604b452174045 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts @@ -34,6 +34,7 @@ export const findThresholdSignals = async ({ }: FindThresholdSignalsParams): Promise<{ searchResult: SignalSearchResponse; searchDuration: string; + searchErrors: string[]; }> => { const aggregations = threshold && !isEmpty(threshold.field) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index 756aedd5273d3..d369a91335347 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -3,56 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; -import { AlertServices } from '../../../../../alerts/server'; -import { ListClient } from '../../../../../lists/server'; -import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { RuleTypeParams, RefreshTypes } from '../types'; -import { Logger } from '../../../../../../../src/core/server'; import { singleSearchAfter } from './single_search_after'; import { singleBulkCreate } from './single_bulk_create'; -import { BuildRuleMessage } from './rule_messages'; -import { SignalSearchResponse } from './types'; import { filterEventsAgainstList } from './filter_events_with_list'; -import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; -import { getSignalTimeTuples } from './utils'; - -interface SearchAfterAndBulkCreateParams { - gap: moment.Duration | null; - previousStartedAt: Date | null | undefined; - ruleParams: RuleTypeParams; - services: AlertServices; - listClient: ListClient; - exceptionsList: ExceptionListItemSchema[]; - logger: Logger; - id: string; - inputIndexPattern: string[]; - signalsIndex: string; - name: string; - actions: RuleAlertAction[]; - createdAt: string; - createdBy: string; - updatedBy: string; - updatedAt: string; - interval: string; - enabled: boolean; - pageSize: number; - filter: unknown; - refresh: RefreshTypes; - tags: string[]; - throttle: string; - buildRuleMessage: BuildRuleMessage; -} - -export interface SearchAfterAndBulkCreateReturnType { - success: boolean; - searchAfterTimes: string[]; - bulkCreateTimes: string[]; - lastLookBackDate: Date | null | undefined; - createdSignalsCount: number; - errors: string[]; -} +import { + createSearchAfterReturnType, + createSearchAfterReturnTypeFromResponse, + createTotalHitsFromSearchResult, + getSignalTimeTuples, + mergeReturns, +} from './utils'; +import { SearchAfterAndBulkCreateParams, SearchAfterAndBulkCreateReturnType } from './types'; // search_after through documents and re-index using bulk endpoint. export const searchAfterAndBulkCreate = async ({ @@ -81,14 +43,7 @@ export const searchAfterAndBulkCreate = async ({ throttle, buildRuleMessage, }: SearchAfterAndBulkCreateParams): Promise => { - const toReturn: SearchAfterAndBulkCreateReturnType = { - success: true, - searchAfterTimes: [], - bulkCreateTimes: [], - lastLookBackDate: null, - createdSignalsCount: 0, - errors: [], - }; + let toReturn = createSearchAfterReturnType(); // sortId tells us where to start our next consecutive search_after query let sortId: string | undefined; @@ -108,13 +63,15 @@ export const searchAfterAndBulkCreate = async ({ buildRuleMessage, }); logger.debug(buildRuleMessage(`totalToFromTuples: ${totalToFromTuples.length}`)); + while (totalToFromTuples.length > 0) { const tuple = totalToFromTuples.pop(); if (tuple == null || tuple.to == null || tuple.from == null) { logger.error(buildRuleMessage(`[-] malformed date tuple`)); - toReturn.success = false; - toReturn.errors = [...new Set([...toReturn.errors, 'malformed date tuple'])]; - return toReturn; + return createSearchAfterReturnType({ + success: false, + errors: ['malformed date tuple'], + }); } signalsCreatedCount = 0; while (signalsCreatedCount < tuple.maxSignals) { @@ -122,29 +79,27 @@ export const searchAfterAndBulkCreate = async ({ logger.debug(buildRuleMessage(`sortIds: ${sortId}`)); // perform search_after with optionally undefined sortId - const { - searchResult, - searchDuration, - }: { searchResult: SignalSearchResponse; searchDuration: string } = await singleSearchAfter( - { - searchAfterSortId: sortId, - index: inputIndexPattern, - from: tuple.from.toISOString(), - to: tuple.to.toISOString(), - services, - logger, - filter, - pageSize: tuple.maxSignals < pageSize ? Math.ceil(tuple.maxSignals) : pageSize, // maximum number of docs to receive per search result. - timestampOverride: ruleParams.timestampOverride, - } - ); - toReturn.searchAfterTimes.push(searchDuration); - + const { searchResult, searchDuration, searchErrors } = await singleSearchAfter({ + searchAfterSortId: sortId, + index: inputIndexPattern, + from: tuple.from.toISOString(), + to: tuple.to.toISOString(), + services, + logger, + filter, + pageSize: tuple.maxSignals < pageSize ? Math.ceil(tuple.maxSignals) : pageSize, // maximum number of docs to receive per search result. + timestampOverride: ruleParams.timestampOverride, + }); + toReturn = mergeReturns([ + toReturn, + createSearchAfterReturnTypeFromResponse({ searchResult }), + createSearchAfterReturnType({ + searchAfterTimes: [searchDuration], + errors: searchErrors, + }), + ]); // determine if there are any candidate signals to be processed - const totalHits = - typeof searchResult.hits.total === 'number' - ? searchResult.hits.total - : searchResult.hits.total.value; + const totalHits = createTotalHitsFromSearchResult({ searchResult }); logger.debug(buildRuleMessage(`totalHits: ${totalHits}`)); logger.debug( buildRuleMessage(`searchResult.hit.hits.length: ${searchResult.hits.hits.length}`) @@ -168,17 +123,11 @@ export const searchAfterAndBulkCreate = async ({ ); break; } - toReturn.lastLookBackDate = - searchResult.hits.hits.length > 0 - ? new Date( - searchResult.hits.hits[searchResult.hits.hits.length - 1]?._source['@timestamp'] - ) - : null; // filter out the search results that match with the values found in the list. // the resulting set are signals to be indexed, given they are not duplicates // of signals already present in the signals index. - const filteredEvents: SignalSearchResponse = await filterEventsAgainstList({ + const filteredEvents = await filterEventsAgainstList({ listClient, exceptionsList, logger, @@ -222,19 +171,21 @@ export const searchAfterAndBulkCreate = async ({ tags, throttle, }); - logger.debug(buildRuleMessage(`created ${createdCount} signals`)); - toReturn.createdSignalsCount += createdCount; + toReturn = mergeReturns([ + toReturn, + createSearchAfterReturnType({ + success: bulkSuccess, + createdSignalsCount: createdCount, + bulkCreateTimes: bulkDuration ? [bulkDuration] : undefined, + errors: bulkErrors, + }), + ]); signalsCreatedCount += createdCount; + logger.debug(buildRuleMessage(`created ${createdCount} signals`)); logger.debug(buildRuleMessage(`signalsCreatedCount: ${signalsCreatedCount}`)); - if (bulkDuration) { - toReturn.bulkCreateTimes.push(bulkDuration); - } - logger.debug( buildRuleMessage(`filteredEvents.hits.hits: ${filteredEvents.hits.hits.length}`) ); - toReturn.success = toReturn.success && bulkSuccess; - toReturn.errors = [...new Set([...toReturn.errors, ...bulkErrors])]; } // we are guaranteed to have searchResult hits at this point @@ -249,9 +200,13 @@ export const searchAfterAndBulkCreate = async ({ } } catch (exc: unknown) { logger.error(buildRuleMessage(`[-] search_after and bulk threw an error ${exc}`)); - toReturn.success = false; - toReturn.errors = [...new Set([...toReturn.errors, `${exc}`])]; - return toReturn; + return mergeReturns([ + toReturn, + createSearchAfterReturnType({ + success: false, + errors: [`${exc}`], + }), + ]); } } } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 3ff5d5d2a6e13..382acf2f38245 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -18,11 +18,8 @@ import { sortExceptionItems, } from './utils'; import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; -import { RuleExecutorOptions } from './types'; -import { - searchAfterAndBulkCreate, - SearchAfterAndBulkCreateReturnType, -} from './search_after_bulk_create'; +import { RuleExecutorOptions, SearchAfterAndBulkCreateReturnType } from './types'; +import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { scheduleNotificationActions } from '../notifications/schedule_notification_actions'; import { RuleAlertType } from '../rules/types'; import { findMlSignals } from './find_ml_signals'; @@ -36,7 +33,17 @@ jest.mock('./rule_status_saved_objects_client'); jest.mock('./rule_status_service'); jest.mock('./search_after_bulk_create'); jest.mock('./get_filter'); -jest.mock('./utils'); +jest.mock('./utils', () => { + const original = jest.requireActual('./utils'); + return { + ...original, + getGapBetweenRuns: jest.fn(), + getGapMaxCatchupRatio: jest.fn(), + getListsClient: jest.fn(), + getExceptions: jest.fn(), + sortExceptionItems: jest.fn(), + }; +}); jest.mock('../notifications/schedule_notification_actions'); jest.mock('./find_ml_signals'); jest.mock('./bulk_create_ml_signals'); @@ -383,6 +390,7 @@ describe('rules_notification_alert_type', () => { }, ]); (findMlSignals as jest.Mock).mockResolvedValue({ + _shards: {}, hits: { hits: [], }, @@ -401,6 +409,7 @@ describe('rules_notification_alert_type', () => { payload = getPayload(ruleAlert, alertServices) as jest.Mocked; jobsSummaryMock.mockResolvedValue([]); (findMlSignals as jest.Mock).mockResolvedValue({ + _shards: {}, hits: { hits: [], }, @@ -409,6 +418,7 @@ describe('rules_notification_alert_type', () => { success: true, bulkCreateDuration: 0, createdItemsCount: 0, + errors: [], }); await alert.executor(payload); expect(ruleStatusService.success).not.toHaveBeenCalled(); @@ -425,6 +435,7 @@ describe('rules_notification_alert_type', () => { }, ]); (findMlSignals as jest.Mock).mockResolvedValue({ + _shards: { failed: 0 }, hits: { hits: [{}], }, @@ -433,6 +444,7 @@ describe('rules_notification_alert_type', () => { success: true, bulkCreateDuration: 1, createdItemsCount: 1, + errors: [], }); await alert.executor(payload); expect(ruleStatusService.success).toHaveBeenCalled(); @@ -460,6 +472,7 @@ describe('rules_notification_alert_type', () => { }); jobsSummaryMock.mockResolvedValue([]); (findMlSignals as jest.Mock).mockResolvedValue({ + _shards: { failed: 0 }, hits: { hits: [{}], }, @@ -468,6 +481,7 @@ describe('rules_notification_alert_type', () => { success: true, bulkCreateDuration: 1, createdItemsCount: 1, + errors: [], }); await alert.executor(payload); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 196c17b42221b..97ab12f905358 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -22,10 +22,7 @@ import { import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { SetupPlugins } from '../../../plugin'; import { getInputIndex } from './get_input_output_index'; -import { - searchAfterAndBulkCreate, - SearchAfterAndBulkCreateReturnType, -} from './search_after_bulk_create'; +import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { getFilter } from './get_filter'; import { SignalRuleAlertTypeDefinition, RuleAlertAttributes } from './types'; import { @@ -34,6 +31,10 @@ import { getExceptions, getGapMaxCatchupRatio, MAX_RULE_GAP_RATIO, + createErrorsFromShard, + createSearchAfterReturnType, + mergeReturns, + createSearchAfterReturnTypeFromResponse, } from './utils'; import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; @@ -104,14 +105,7 @@ export const signalRulesAlertType = ({ } = params; const searchAfterSize = Math.min(maxSignals, DEFAULT_SEARCH_AFTER_PAGE_SIZE); let hasError: boolean = false; - let result: SearchAfterAndBulkCreateReturnType = { - success: false, - bulkCreateTimes: [], - searchAfterTimes: [], - lastLookBackDate: null, - createdSignalsCount: 0, - errors: [], - }; + let result = createSearchAfterReturnType(); const ruleStatusClient = ruleStatusSavedObjectsClientFactory(services.savedObjectsClient); const ruleStatusService = await ruleStatusServiceFactory({ alertId, @@ -255,12 +249,22 @@ export const signalRulesAlertType = ({ refresh, tags, }); - result.success = success; - result.errors = errors; - result.createdSignalsCount = createdItemsCount; - if (bulkCreateDuration) { - result.bulkCreateTimes.push(bulkCreateDuration); - } + // The legacy ES client does not define failures when it can be present on the structure, hence why I have the & { failures: [] } + const shardFailures = + (anomalyResults._shards as typeof anomalyResults._shards & { failures: [] }).failures ?? + []; + const searchErrors = createErrorsFromShard({ + errors: shardFailures, + }); + result = mergeReturns([ + result, + createSearchAfterReturnType({ + success: success && anomalyResults._shards.failed === 0, + errors: [...errors, ...searchErrors], + createdSignalsCount: createdItemsCount, + bulkCreateTimes: bulkCreateDuration ? [bulkCreateDuration] : [], + }), + ]); } else if (isEqlRule(type)) { throw new Error('EQL Rules are under development, execution is not yet implemented'); } else if (isThresholdRule(type) && threshold) { @@ -276,7 +280,7 @@ export const signalRulesAlertType = ({ lists: exceptionItems ?? [], }); - const { searchResult: thresholdResults } = await findThresholdSignals({ + const { searchResult: thresholdResults, searchErrors } = await findThresholdSignals({ inputIndexPattern: inputIndex, from, to, @@ -313,12 +317,16 @@ export const signalRulesAlertType = ({ refresh, tags, }); - result.success = success; - result.errors = errors; - result.createdSignalsCount = createdItemsCount; - if (bulkCreateDuration) { - result.bulkCreateTimes.push(bulkCreateDuration); - } + result = mergeReturns([ + result, + createSearchAfterReturnTypeFromResponse({ searchResult: thresholdResults }), + createSearchAfterReturnType({ + success, + errors: [...errors, ...searchErrors], + createdSignalsCount: createdItemsCount, + bulkCreateTimes: bulkCreateDuration ? [bulkCreateDuration] : [], + }), + ]); } else if (isThreatMatchRule(type)) { if ( threatQuery == null || diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts index 250b891eb1f2c..da81911f07ad9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts @@ -11,6 +11,7 @@ import { } from './__mocks__/es_results'; import { singleSearchAfter } from './single_search_after'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; +import { ShardError } from '../../types'; describe('singleSearchAfter', () => { const mockService: AlertServicesMock = alertsMock.createAlertServices(); @@ -20,10 +21,71 @@ describe('singleSearchAfter', () => { }); test('if singleSearchAfter works without a given sort id', async () => { - let searchAfterSortId; - mockService.callCluster.mockResolvedValue(sampleDocSearchResultsNoSortId); + mockService.callCluster.mockResolvedValue(sampleDocSearchResultsNoSortId()); const { searchResult } = await singleSearchAfter({ - searchAfterSortId, + searchAfterSortId: undefined, + index: [], + from: 'now-360s', + to: 'now', + services: mockService, + logger: mockLogger, + pageSize: 1, + filter: undefined, + timestampOverride: undefined, + }); + expect(searchResult).toEqual(sampleDocSearchResultsNoSortId()); + }); + test('if singleSearchAfter returns an empty failure array', async () => { + mockService.callCluster.mockResolvedValue(sampleDocSearchResultsNoSortId()); + const { searchErrors } = await singleSearchAfter({ + searchAfterSortId: undefined, + index: [], + from: 'now-360s', + to: 'now', + services: mockService, + logger: mockLogger, + pageSize: 1, + filter: undefined, + timestampOverride: undefined, + }); + expect(searchErrors).toEqual([]); + }); + test('if singleSearchAfter will return an error array', async () => { + const errors: ShardError[] = [ + { + shard: 1, + index: 'index-123', + node: 'node-123', + reason: { + type: 'some type', + reason: 'some reason', + index_uuid: 'uuid-123', + index: 'index-123', + caused_by: { + type: 'some type', + reason: 'some reason', + }, + }, + }, + ]; + mockService.callCluster.mockResolvedValue({ + took: 10, + timed_out: false, + _shards: { + total: 10, + successful: 10, + failed: 1, + skipped: 0, + failures: errors, + }, + hits: { + total: 100, + max_score: 100, + hits: [], + }, + }); + const { searchErrors } = await singleSearchAfter({ + searchAfterSortId: undefined, index: [], from: 'now-360s', to: 'now', @@ -33,11 +95,11 @@ describe('singleSearchAfter', () => { filter: undefined, timestampOverride: undefined, }); - expect(searchResult).toEqual(sampleDocSearchResultsNoSortId); + expect(searchErrors).toEqual(['reason: some reason, type: some type, caused by: some reason']); }); test('if singleSearchAfter works with a given sort id', async () => { const searchAfterSortId = '1234567891111'; - mockService.callCluster.mockResolvedValue(sampleDocSearchResultsWithSortId); + mockService.callCluster.mockResolvedValue(sampleDocSearchResultsWithSortId()); const { searchResult } = await singleSearchAfter({ searchAfterSortId, index: [], @@ -49,7 +111,7 @@ describe('singleSearchAfter', () => { filter: undefined, timestampOverride: undefined, }); - expect(searchResult).toEqual(sampleDocSearchResultsWithSortId); + expect(searchResult).toEqual(sampleDocSearchResultsWithSortId()); }); test('if singleSearchAfter throws error', async () => { const searchAfterSortId = '1234567891111'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts index 92ce7a2836115..f758adb21611c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts @@ -9,7 +9,7 @@ import { AlertServices } from '../../../../../alerts/server'; import { Logger } from '../../../../../../../src/core/server'; import { SignalSearchResponse } from './types'; import { buildEventsSearchQuery } from './build_events_query'; -import { makeFloatString } from './utils'; +import { createErrorsFromShard, makeFloatString } from './utils'; import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; interface SingleSearchAfterParams { @@ -40,6 +40,7 @@ export const singleSearchAfter = async ({ }: SingleSearchAfterParams): Promise<{ searchResult: SignalSearchResponse; searchDuration: string; + searchErrors: string[]; }> => { try { const searchAfterQuery = buildEventsSearchQuery({ @@ -59,7 +60,14 @@ export const singleSearchAfter = async ({ searchAfterQuery ); const end = performance.now(); - return { searchResult: nextSearchAfterResult, searchDuration: makeFloatString(end - start) }; + const searchErrors = createErrorsFromShard({ + errors: nextSearchAfterResult._shards.failures ?? [], + }); + return { + searchResult: nextSearchAfterResult, + searchDuration: makeFloatString(end - start), + searchErrors, + }; } catch (exc) { logger.error(`[-] nextSearchAfter threw an error ${exc}`); throw exc; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts index 7542128d83769..a6d4a2ba58ddd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts @@ -9,12 +9,10 @@ import { getThreatList } from './get_threat_list'; import { buildThreatMappingFilter } from './build_threat_mapping_filter'; import { getFilter } from '../get_filter'; -import { - searchAfterAndBulkCreate, - SearchAfterAndBulkCreateReturnType, -} from '../search_after_bulk_create'; +import { searchAfterAndBulkCreate } from '../search_after_bulk_create'; import { CreateThreatSignalOptions, ThreatListItem } from './types'; import { combineResults } from './utils'; +import { SearchAfterAndBulkCreateReturnType } from '../types'; export const createThreatSignal = async ({ threatMapping, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts index 9027475d71c4a..f416ae6703b66 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts @@ -6,9 +6,9 @@ import { getThreatList } from './get_threat_list'; -import { SearchAfterAndBulkCreateReturnType } from '../search_after_bulk_create'; import { CreateThreatSignalsOptions } from './types'; import { createThreatSignal } from './create_threat_signal'; +import { SearchAfterAndBulkCreateReturnType } from '../types'; export const createThreatSignals = async ({ threatMapping, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts index 4c3cd9943adb4..d63f2d2b3b6aa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts @@ -19,10 +19,10 @@ import { import { PartialFilter, RuleTypeParams } from '../../types'; import { AlertServices } from '../../../../../../alerts/server'; import { ExceptionListItemSchema } from '../../../../../../lists/common/schemas'; -import { SearchAfterAndBulkCreateReturnType } from '../search_after_bulk_create'; import { ILegacyScopedClusterClient, Logger } from '../../../../../../../../src/core/server'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { BuildRuleMessage } from '../rule_messages'; +import { SearchAfterAndBulkCreateReturnType } from '../types'; export interface CreateThreatSignalsOptions { threatMapping: ThreatMapping; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts index 48bdf430b940e..27593b40b0c8f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchAfterAndBulkCreateReturnType } from '../search_after_bulk_create'; +import { SearchAfterAndBulkCreateReturnType } from '../types'; import { calculateAdditiveMax, combineResults } from './utils'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts index 38bbb70b6c4ec..401a4a1acb065 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchAfterAndBulkCreateReturnType } from '../search_after_bulk_create'; +import { SearchAfterAndBulkCreateReturnType } from '../types'; /** * Given two timers this will take the max of each and add them to each other and return that addition. diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 23aa786558a99..6ebdca0764e9d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -5,12 +5,22 @@ */ import { DslQuery, Filter } from 'src/plugins/data/common'; +import moment from 'moment'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; -import { AlertType, AlertTypeState, AlertExecutorOptions } from '../../../../../alerts/server'; +import { + AlertType, + AlertTypeState, + AlertExecutorOptions, + AlertServices, +} from '../../../../../alerts/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { RuleTypeParams } from '../types'; +import { RuleTypeParams, RefreshTypes } from '../types'; import { SearchResponse } from '../../types'; +import { ListClient } from '../../../../../lists/server'; +import { Logger } from '../../../../../../../src/core/server'; +import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; +import { BuildRuleMessage } from './rule_messages'; // used for gap detection code // eslint-disable-next-line @typescript-eslint/naming-convention @@ -179,3 +189,39 @@ export interface QueryFilter { must_not: Filter[]; }; } + +export interface SearchAfterAndBulkCreateParams { + gap: moment.Duration | null; + previousStartedAt: Date | null | undefined; + ruleParams: RuleTypeParams; + services: AlertServices; + listClient: ListClient; + exceptionsList: ExceptionListItemSchema[]; + logger: Logger; + id: string; + inputIndexPattern: string[]; + signalsIndex: string; + name: string; + actions: RuleAlertAction[]; + createdAt: string; + createdBy: string; + updatedBy: string; + updatedAt: string; + interval: string; + enabled: boolean; + pageSize: number; + filter: unknown; + refresh: RefreshTypes; + tags: string[]; + throttle: string; + buildRuleMessage: BuildRuleMessage; +} + +export interface SearchAfterAndBulkCreateReturnType { + success: boolean; + searchAfterTimes: string[]; + bulkCreateTimes: string[]; + lastLookBackDate: Date | null | undefined; + createdSignalsCount: number; + errors: string[]; +} 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 123b9c9bdffa2..97f3dbeaf4489 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 @@ -25,15 +25,25 @@ import { getListsClient, getSignalTimeTuples, getExceptions, + createErrorsFromShard, + createSearchAfterReturnTypeFromResponse, + createSearchAfterReturnType, + mergeReturns, + createTotalHitsFromSearchResult, } from './utils'; -import { BulkResponseErrorAggregation } from './types'; +import { BulkResponseErrorAggregation, SearchAfterAndBulkCreateReturnType } from './types'; import { sampleBulkResponse, sampleEmptyBulkResponse, sampleBulkError, sampleBulkErrorItem, mockLogger, + sampleDocSearchResultsWithSortId, + sampleEmptyDocSearchResults, + sampleDocSearchResultsNoSortIdNoHits, + repeatedSearchResultsWithSortId, } from './__mocks__/es_results'; +import { ShardError } from '../../types'; const buildRuleMessage = buildRuleMessageFactory({ id: 'fake id', @@ -783,4 +793,278 @@ describe('utils', () => { expect(exceptions).toEqual([]); }); }); + + describe('createErrorsFromShard', () => { + test('empty errors will return an empty array', () => { + const createdErrors = createErrorsFromShard({ errors: [] }); + expect(createdErrors).toEqual([]); + }); + + test('single error will return single converted array of a string of a reason', () => { + const errors: ShardError[] = [ + { + shard: 1, + index: 'index-123', + node: 'node-123', + reason: { + type: 'some type', + reason: 'some reason', + index_uuid: 'uuid-123', + index: 'index-123', + caused_by: { + type: 'some type', + reason: 'some reason', + }, + }, + }, + ]; + const createdErrors = createErrorsFromShard({ errors }); + expect(createdErrors).toEqual([ + 'reason: some reason, type: some type, caused by: some reason', + ]); + }); + + test('two errors will return two converted arrays to a string of a reason', () => { + const errors: ShardError[] = [ + { + shard: 1, + index: 'index-123', + node: 'node-123', + reason: { + type: 'some type', + reason: 'some reason', + index_uuid: 'uuid-123', + index: 'index-123', + caused_by: { + type: 'some type', + reason: 'some reason', + }, + }, + }, + { + shard: 2, + index: 'index-345', + node: 'node-345', + reason: { + type: 'some type 2', + reason: 'some reason 2', + index_uuid: 'uuid-345', + index: 'index-345', + caused_by: { + type: 'some type 2', + reason: 'some reason 2', + }, + }, + }, + ]; + const createdErrors = createErrorsFromShard({ errors }); + expect(createdErrors).toEqual([ + 'reason: some reason, type: some type, caused by: some reason', + 'reason: some reason 2, type: some type 2, caused by: some reason 2', + ]); + }); + }); + + describe('createSearchAfterReturnTypeFromResponse', () => { + test('empty results will return successful type', () => { + const searchResult = sampleEmptyDocSearchResults(); + const newSearchResult = createSearchAfterReturnTypeFromResponse({ searchResult }); + const expected: SearchAfterAndBulkCreateReturnType = { + bulkCreateTimes: [], + createdSignalsCount: 0, + errors: [], + lastLookBackDate: null, + searchAfterTimes: [], + success: true, + }; + expect(newSearchResult).toEqual(expected); + }); + + test('multiple results will return successful type with expected success', () => { + const searchResult = sampleDocSearchResultsWithSortId(); + const newSearchResult = createSearchAfterReturnTypeFromResponse({ searchResult }); + const expected: SearchAfterAndBulkCreateReturnType = { + bulkCreateTimes: [], + createdSignalsCount: 0, + errors: [], + lastLookBackDate: new Date('2020-04-20T21:27:45.000Z'), + searchAfterTimes: [], + success: true, + }; + expect(newSearchResult).toEqual(expected); + }); + + test('result with error will create success: false within the result set', () => { + const searchResult = sampleDocSearchResultsNoSortIdNoHits(); + searchResult._shards.failed = 1; + const { success } = createSearchAfterReturnTypeFromResponse({ searchResult }); + expect(success).toEqual(false); + }); + + test('result with error will create success: false within the result set if failed is 2 or more', () => { + const searchResult = sampleDocSearchResultsNoSortIdNoHits(); + searchResult._shards.failed = 2; + const { success } = createSearchAfterReturnTypeFromResponse({ searchResult }); + expect(success).toEqual(false); + }); + + test('result with error will create success: true within the result set if failed is 0', () => { + const searchResult = sampleDocSearchResultsNoSortIdNoHits(); + searchResult._shards.failed = 0; + const { success } = createSearchAfterReturnTypeFromResponse({ searchResult }); + expect(success).toEqual(true); + }); + }); + + describe('createSearchAfterReturnType', () => { + test('createSearchAfterReturnType will return full object when nothing is passed', () => { + const searchAfterReturnType = createSearchAfterReturnType(); + const expected: SearchAfterAndBulkCreateReturnType = { + bulkCreateTimes: [], + createdSignalsCount: 0, + errors: [], + lastLookBackDate: null, + searchAfterTimes: [], + success: true, + }; + expect(searchAfterReturnType).toEqual(expected); + }); + + test('createSearchAfterReturnType can override all values', () => { + const searchAfterReturnType = createSearchAfterReturnType({ + bulkCreateTimes: ['123'], + createdSignalsCount: 5, + errors: ['error 1'], + lastLookBackDate: new Date('2020-09-21T18:51:25.193Z'), + searchAfterTimes: ['123'], + success: false, + }); + const expected: SearchAfterAndBulkCreateReturnType = { + bulkCreateTimes: ['123'], + createdSignalsCount: 5, + errors: ['error 1'], + lastLookBackDate: new Date('2020-09-21T18:51:25.193Z'), + searchAfterTimes: ['123'], + success: false, + }; + expect(searchAfterReturnType).toEqual(expected); + }); + + test('createSearchAfterReturnType can override select values', () => { + const searchAfterReturnType = createSearchAfterReturnType({ + createdSignalsCount: 5, + errors: ['error 1'], + }); + const expected: SearchAfterAndBulkCreateReturnType = { + bulkCreateTimes: [], + createdSignalsCount: 5, + errors: ['error 1'], + lastLookBackDate: null, + searchAfterTimes: [], + success: true, + }; + expect(searchAfterReturnType).toEqual(expected); + }); + }); + + describe('mergeReturns', () => { + test('it merges a default "prev" and "next" correctly ', () => { + const merged = mergeReturns([createSearchAfterReturnType(), createSearchAfterReturnType()]); + const expected: SearchAfterAndBulkCreateReturnType = { + bulkCreateTimes: [], + createdSignalsCount: 0, + errors: [], + lastLookBackDate: null, + searchAfterTimes: [], + success: true, + }; + expect(merged).toEqual(expected); + }); + + test('it merges search in with two default search results where "prev" "success" is false correctly', () => { + const { success } = mergeReturns([ + createSearchAfterReturnType({ success: false }), + createSearchAfterReturnType(), + ]); + expect(success).toEqual(false); + }); + + test('it merges search in with two default search results where "next" "success" is false correctly', () => { + const { success } = mergeReturns([ + createSearchAfterReturnType(), + createSearchAfterReturnType({ success: false }), + ]); + expect(success).toEqual(false); + }); + + test('it merges search where the lastLookBackDate is the "next" date when given', () => { + const { lastLookBackDate } = mergeReturns([ + createSearchAfterReturnType({ + lastLookBackDate: new Date('2020-08-21T19:21:46.194Z'), + }), + createSearchAfterReturnType({ + lastLookBackDate: new Date('2020-09-21T19:21:46.194Z'), + }), + ]); + expect(lastLookBackDate).toEqual(new Date('2020-09-21T19:21:46.194Z')); + }); + + test('it merges search where the lastLookBackDate is the "prev" if given undefined for "next', () => { + const { lastLookBackDate } = mergeReturns([ + createSearchAfterReturnType({ + lastLookBackDate: new Date('2020-08-21T19:21:46.194Z'), + }), + createSearchAfterReturnType({ + lastLookBackDate: undefined, + }), + ]); + expect(lastLookBackDate).toEqual(new Date('2020-08-21T19:21:46.194Z')); + }); + + test('it merges search where values from "next" and "prev" are computed together', () => { + const merged = mergeReturns([ + createSearchAfterReturnType({ + bulkCreateTimes: ['123'], + createdSignalsCount: 3, + errors: ['error 1', 'error 2'], + lastLookBackDate: new Date('2020-08-21T18:51:25.193Z'), + searchAfterTimes: ['123'], + success: true, + }), + createSearchAfterReturnType({ + bulkCreateTimes: ['456'], + createdSignalsCount: 2, + errors: ['error 3'], + lastLookBackDate: new Date('2020-09-21T18:51:25.193Z'), + searchAfterTimes: ['567'], + success: true, + }), + ]); + const expected: SearchAfterAndBulkCreateReturnType = { + bulkCreateTimes: ['123', '456'], // concatenates the prev and next together + createdSignalsCount: 5, // Adds the 3 and 2 together + errors: ['error 1', 'error 2', 'error 3'], // concatenates the prev and next together + lastLookBackDate: new Date('2020-09-21T18:51:25.193Z'), // takes the next lastLookBackDate + searchAfterTimes: ['123', '567'], // concatenates the searchAfterTimes together + success: true, // Defaults to success true is all of it was successful + }; + expect(merged).toEqual(expected); + }); + }); + + describe('createTotalHitsFromSearchResult', () => { + test('it should return 0 for empty results', () => { + const result = createTotalHitsFromSearchResult({ + searchResult: sampleEmptyDocSearchResults(), + }); + expect(result).toEqual(0); + }); + + test('it should return 4 for 4 result sets', () => { + const result = createTotalHitsFromSearchResult({ + searchResult: repeatedSearchResultsWithSortId(4, 1, ['1', '2', '3', '4']), + }); + expect(result).toEqual(4); + }); + }); }); 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 9f1e5d6980466..2eabc03dccad7 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 @@ -12,11 +12,18 @@ import { AlertServices, parseDuration } from '../../../../../alerts/server'; import { ExceptionListClient, ListClient, ListPluginSetup } from '../../../../../lists/server'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { ListArray } from '../../../../common/detection_engine/schemas/types/lists'; -import { BulkResponse, BulkResponseErrorAggregation, isValidUnit } from './types'; +import { + BulkResponse, + BulkResponseErrorAggregation, + isValidUnit, + SearchAfterAndBulkCreateReturnType, + SignalSearchResponse, +} from './types'; import { BuildRuleMessage } from './rule_messages'; import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { hasLargeValueList } from '../../../../common/detection_engine/utils'; import { MAX_EXCEPTION_LIST_SIZE } from '../../../../../lists/common/constants'; +import { ShardError } from '../../types'; interface SortExceptionsReturn { exceptionsWithValueLists: ExceptionListItemSchema[]; @@ -439,3 +446,97 @@ export const getSignalTimeTuples = ({ ); return totalToFromTuples; }; + +/** + * Given errors from a search query this will return an array of strings derived from the errors. + * @param errors The errors to derive the strings from + */ +export const createErrorsFromShard = ({ errors }: { errors: ShardError[] }): string[] => { + return errors.map((error) => { + return `reason: ${error.reason.reason}, type: ${error.reason.caused_by.type}, caused by: ${error.reason.caused_by.reason}`; + }); +}; + +export const createSearchAfterReturnTypeFromResponse = ({ + searchResult, +}: { + searchResult: SignalSearchResponse; +}): SearchAfterAndBulkCreateReturnType => { + return createSearchAfterReturnType({ + success: searchResult._shards.failed === 0, + lastLookBackDate: + searchResult.hits.hits.length > 0 + ? new Date(searchResult.hits.hits[searchResult.hits.hits.length - 1]?._source['@timestamp']) + : undefined, + }); +}; + +export const createSearchAfterReturnType = ({ + success, + searchAfterTimes, + bulkCreateTimes, + lastLookBackDate, + createdSignalsCount, + errors, +}: { + success?: boolean | undefined; + searchAfterTimes?: string[] | undefined; + bulkCreateTimes?: string[] | undefined; + lastLookBackDate?: Date | undefined; + createdSignalsCount?: number | undefined; + errors?: string[] | undefined; +} = {}): SearchAfterAndBulkCreateReturnType => { + return { + success: success ?? true, + searchAfterTimes: searchAfterTimes ?? [], + bulkCreateTimes: bulkCreateTimes ?? [], + lastLookBackDate: lastLookBackDate ?? null, + createdSignalsCount: createdSignalsCount ?? 0, + errors: errors ?? [], + }; +}; + +export const mergeReturns = ( + searchAfters: SearchAfterAndBulkCreateReturnType[] +): SearchAfterAndBulkCreateReturnType => { + return searchAfters.reduce((prev, next) => { + const { + success: existingSuccess, + searchAfterTimes: existingSearchAfterTimes, + bulkCreateTimes: existingBulkCreateTimes, + lastLookBackDate: existingLastLookBackDate, + createdSignalsCount: existingCreatedSignalsCount, + errors: existingErrors, + } = prev; + + const { + success: newSuccess, + searchAfterTimes: newSearchAfterTimes, + bulkCreateTimes: newBulkCreateTimes, + lastLookBackDate: newLastLookBackDate, + createdSignalsCount: newCreatedSignalsCount, + errors: newErrors, + } = next; + + return { + success: existingSuccess && newSuccess, + searchAfterTimes: [...existingSearchAfterTimes, ...newSearchAfterTimes], + bulkCreateTimes: [...existingBulkCreateTimes, ...newBulkCreateTimes], + lastLookBackDate: newLastLookBackDate ?? existingLastLookBackDate, + createdSignalsCount: existingCreatedSignalsCount + newCreatedSignalsCount, + errors: [...new Set([...existingErrors, ...newErrors])], + }; + }); +}; + +export const createTotalHitsFromSearchResult = ({ + searchResult, +}: { + searchResult: SignalSearchResponse; +}): number => { + const totalHits = + typeof searchResult.hits.total === 'number' + ? searchResult.hits.total + : searchResult.hits.total.value; + return totalHits; +}; diff --git a/x-pack/plugins/security_solution/server/lib/types.ts b/x-pack/plugins/security_solution/server/lib/types.ts index ff89512124b66..435bcd9d61d89 100644 --- a/x-pack/plugins/security_solution/server/lib/types.ts +++ b/x-pack/plugins/security_solution/server/lib/types.ts @@ -98,6 +98,23 @@ export interface ShardsResponse { successful: number; failed: number; skipped: number; + failures?: ShardError[]; +} + +export interface ShardError { + shard: number; + index: string; + node: string; + reason: { + type: string; + reason: string; + index_uuid: string; + index: string; + caused_by: { + type: string; + reason: string; + }; + }; } export interface Explanation { From f3b35c552ea152674c5d53a158384f339f6689b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Wed, 23 Sep 2020 08:34:52 +0200 Subject: [PATCH 29/92] Bump react-beautiful-dnd (#78028) * Bump react-beautiful-dnd * fix types * fix types --- x-pack/package.json | 4 ++-- .../timeline/body/column_headers/index.tsx | 6 ++--- .../timelines/components/timeline/styles.tsx | 4 ++-- yarn.lock | 22 +------------------ 4 files changed, 7 insertions(+), 29 deletions(-) diff --git a/x-pack/package.json b/x-pack/package.json index 6593f04bade27..3af97ed16ed6f 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -111,7 +111,7 @@ "@types/proper-lockfile": "^3.0.1", "@types/puppeteer": "^1.20.1", "@types/react": "^16.9.36", - "@types/react-beautiful-dnd": "^12.1.1", + "@types/react-beautiful-dnd": "^13.0.0", "@types/react-dom": "^16.9.8", "@types/react-redux": "^7.1.9", "@types/react-router-dom": "^5.1.5", @@ -221,7 +221,7 @@ "proxyquire": "1.8.0", "re-resizable": "^6.1.1", "react-apollo": "^2.1.4", - "react-beautiful-dnd": "^12.2.0", + "react-beautiful-dnd": "^13.0.0", "react-docgen-typescript-loader": "^3.1.1", "react-dropzone": "^4.2.9", "react-fast-compare": "^2.0.4", diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index ea938be91abd1..6e802053ab29f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -114,7 +114,7 @@ export const ColumnHeadersComponent = ({ timelineId, toggleColumn, }: Props) => { - const [draggingIndex, setDraggingIndex] = useState(null); + const [draggingIndex, setDraggingIndex] = useState(null); const { timelineFullScreen, setTimelineFullScreen, @@ -145,9 +145,7 @@ export const ColumnHeadersComponent = ({ const renderClone: DraggableChildrenFn = useCallback( (dragProvided, _dragSnapshot, rubric) => { - // TODO: Remove after github.com/DefinitelyTyped/DefinitelyTyped/pull/43057 is merged - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const index = (rubric as any).source.index; + const index = rubric.source.index; const header = columnHeaders[index]; const onMount = () => setDraggingIndex(index); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx index 5c992fd640a97..234814a68877d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -113,10 +113,10 @@ export const EventsThGroupData = styled.div.attrs(({ className = '' }) => ({ } `; -export const EventsTh = styled.div.attrs(({ className = '' }) => ({ +export const EventsTh = styled.div.attrs<{ role: string }>(({ className = '' }) => ({ className: `siemEventsTable__th ${className}`, role: 'columnheader', -}))` +}))<{ role?: string }>` align-items: center; display: flex; flex-shrink: 0; diff --git a/yarn.lock b/yarn.lock index 48d675eb5dadd..3549c79970bff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -982,7 +982,7 @@ pirates "^4.0.0" source-map-support "^0.5.16" -"@babel/runtime-corejs2@^7.2.0", "@babel/runtime-corejs2@^7.6.3": +"@babel/runtime-corejs2@^7.2.0": version "7.11.2" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.11.2.tgz#700a03945ebad0d31ba6690fc8a6bcc9040faa47" integrity sha512-AC/ciV28adSSpEkBglONBWq4/Lvm6GAZuxIoyVtsnUpZMl0bxLtoChEnYAkP+47KyOCayZanojtflUEUJtR/6Q== @@ -4550,13 +4550,6 @@ "@types/history" "*" "@types/react" "*" -"@types/react-beautiful-dnd@^12.1.1": - version "12.1.1" - resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-12.1.1.tgz#149e638c0f912eee6b74ea419b26bb43d0b1da60" - integrity sha512-CPKynKgGVRK+xmywLMD0qNWamdscxhgf1Um+2oEgN6Qibn1rye3M4p2bdxAMgtOTZ2L81bYl6KGKSzJVboJWeA== - dependencies: - "@types/react" "*" - "@types/react-beautiful-dnd@^13.0.0": version "13.0.0" resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4" @@ -23470,19 +23463,6 @@ react-apollo@^2.1.4: lodash "^4.17.10" prop-types "^15.6.0" -react-beautiful-dnd@^12.2.0: - version "12.2.0" - resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-12.2.0.tgz#e5f6222f9e7934c6ed4ee09024547f9e353ae423" - integrity sha512-s5UrOXNDgeEC+sx65IgbeFlqKKgK3c0UfbrJLWufP34WBheyu5kJ741DtJbsSgPKyNLkqfswpMYr0P8lRj42cA== - dependencies: - "@babel/runtime-corejs2" "^7.6.3" - css-box-model "^1.2.0" - memoize-one "^5.1.1" - raf-schd "^4.0.2" - react-redux "^7.1.1" - redux "^4.0.4" - use-memo-one "^1.1.1" - react-beautiful-dnd@^13.0.0: version "13.0.0" resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#f70cc8ff82b84bc718f8af157c9f95757a6c3b40" From 82af937db9aa8882279b0b9a09736ea1ab27a9e5 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Wed, 23 Sep 2020 09:45:06 +0300 Subject: [PATCH 30/92] Add response status helpers (#78006) Co-authored-by: Elastic Machine --- ...-plugins-data-public.iscompleteresponse.md | 11 +++++ ...gin-plugins-data-public.iserrorresponse.md | 11 +++++ ...n-plugins-data-public.ispartialresponse.md | 11 +++++ .../kibana-plugin-plugins-data-public.md | 3 ++ .../search_examples/public/components/app.tsx | 6 ++- .../data/common/search/es_search/index.ts | 1 + .../data/common/search/es_search/utils.ts | 41 +++++++++++++++++++ src/plugins/data/common/search/index.ts | 1 + src/plugins/data/public/index.ts | 2 +- src/plugins/data/public/public.api.md | 15 +++++++ .../public/search/search_interceptor.ts | 5 ++- .../server/search/es_search_strategy.ts | 9 ++-- .../events/last_event_time/index.ts | 10 +++-- .../containers/matrix_histogram/index.ts | 10 +++-- .../containers/authentications/index.tsx | 10 +++-- .../hosts/containers/hosts/details/_index.tsx | 10 +++-- .../hosts/first_last_seen/index.tsx | 10 +++-- .../public/hosts/containers/hosts/index.tsx | 10 +++-- .../containers/uncommon_processes/index.tsx | 10 +++-- .../network/containers/details/index.tsx | 10 +++-- .../containers/kpi_network/dns/index.tsx | 10 +++-- .../kpi_network/network_events/index.tsx | 10 +++-- .../kpi_network/tls_handshakes/index.tsx | 10 +++-- .../kpi_network/unique_flows/index.tsx | 10 +++-- .../kpi_network/unique_private_ips/index.tsx | 10 +++-- .../network/containers/network_dns/index.tsx | 10 +++-- .../network/containers/network_http/index.tsx | 10 +++-- .../network_top_countries/index.tsx | 10 +++-- .../containers/network_top_n_flow/index.tsx | 10 +++-- .../public/network/containers/tls/index.tsx | 10 +++-- .../public/network/containers/users/index.tsx | 10 +++-- .../containers/overview_host/index.tsx | 10 +++-- .../containers/overview_network/index.tsx | 10 +++-- .../timelines/containers/details/index.tsx | 5 ++- .../public/timelines/containers/index.tsx | 10 +++-- 35 files changed, 265 insertions(+), 76 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iscompleteresponse.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iserrorresponse.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ispartialresponse.md create mode 100644 src/plugins/data/common/search/es_search/utils.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iscompleteresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iscompleteresponse.md new file mode 100644 index 0000000000000..17acf4e0d1be8 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iscompleteresponse.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isCompleteResponse](./kibana-plugin-plugins-data-public.iscompleteresponse.md) + +## isCompleteResponse variable + +Signature: + +```typescript +isCompleteResponse: (response?: IEsSearchResponse | undefined) => boolean | undefined +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iserrorresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iserrorresponse.md new file mode 100644 index 0000000000000..3f9b1d593870d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iserrorresponse.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isErrorResponse](./kibana-plugin-plugins-data-public.iserrorresponse.md) + +## isErrorResponse variable + +Signature: + +```typescript +isErrorResponse: (response?: IEsSearchResponse | undefined) => boolean | undefined +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ispartialresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ispartialresponse.md new file mode 100644 index 0000000000000..9f2f1bbf2f9e0 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ispartialresponse.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isPartialResponse](./kibana-plugin-plugins-data-public.ispartialresponse.md) + +## isPartialResponse variable + +Signature: + +```typescript +isPartialResponse: (response?: IEsSearchResponse | undefined) => boolean | undefined +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 506f141984052..accf46f534e89 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -115,8 +115,11 @@ | [getKbnTypeNames](./kibana-plugin-plugins-data-public.getkbntypenames.md) | Get the esTypes known by all kbnFieldTypes {Array} | | [indexPatterns](./kibana-plugin-plugins-data-public.indexpatterns.md) | | | [injectSearchSourceReferences](./kibana-plugin-plugins-data-public.injectsearchsourcereferences.md) | | +| [isCompleteResponse](./kibana-plugin-plugins-data-public.iscompleteresponse.md) | | +| [isErrorResponse](./kibana-plugin-plugins-data-public.iserrorresponse.md) | | | [isFilter](./kibana-plugin-plugins-data-public.isfilter.md) | | | [isFilters](./kibana-plugin-plugins-data-public.isfilters.md) | | +| [isPartialResponse](./kibana-plugin-plugins-data-public.ispartialresponse.md) | | | [isQuery](./kibana-plugin-plugins-data-public.isquery.md) | | | [isTimeRange](./kibana-plugin-plugins-data-public.istimerange.md) | | | [parseSearchSourceJSON](./kibana-plugin-plugins-data-public.parsesearchsourcejson.md) | | diff --git a/examples/search_examples/public/components/app.tsx b/examples/search_examples/public/components/app.tsx index 704d31d42e640..ab0ce185f0602 100644 --- a/examples/search_examples/public/components/app.tsx +++ b/examples/search_examples/public/components/app.tsx @@ -56,6 +56,8 @@ import { IndexPatternSelect, IndexPattern, IndexPatternField, + isCompleteResponse, + isErrorResponse, } from '../../../../src/plugins/data/public'; interface SearchExamplesAppDeps { @@ -144,7 +146,7 @@ export const SearchExamplesApp = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { setTimeTook(response.rawResponse.took); const avgResult: number | undefined = response.rawResponse.aggregations ? response.rawResponse.aggregations[1].value @@ -162,7 +164,7 @@ export const SearchExamplesApp = ({ text: mountReactNode(message), }); searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { // TODO: Make response error status clearer notifications.toasts.addWarning('An error has occurred'); searchSubscription$.unsubscribe(); diff --git a/src/plugins/data/common/search/es_search/index.ts b/src/plugins/data/common/search/es_search/index.ts index d8f7b5091eb8f..8e8897c7d7517 100644 --- a/src/plugins/data/common/search/es_search/index.ts +++ b/src/plugins/data/common/search/es_search/index.ts @@ -18,3 +18,4 @@ */ export * from './types'; +export * from './utils'; diff --git a/src/plugins/data/common/search/es_search/utils.ts b/src/plugins/data/common/search/es_search/utils.ts new file mode 100644 index 0000000000000..517a0c03cf5c8 --- /dev/null +++ b/src/plugins/data/common/search/es_search/utils.ts @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IEsSearchResponse } from './types'; + +/** + * @returns true if response had an error while executing in ES + */ +export const isErrorResponse = (response?: IEsSearchResponse) => { + return !response || (!response.isRunning && response.isPartial); +}; + +/** + * @returns true if response is completed successfully + */ +export const isCompleteResponse = (response?: IEsSearchResponse) => { + return response && !response.isRunning && !response.isPartial; +}; + +/** + * @returns true if request is still running an/d response contains partial results + */ +export const isPartialResponse = (response?: IEsSearchResponse) => { + return response && response.isRunning && response.isPartial; +}; diff --git a/src/plugins/data/common/search/index.ts b/src/plugins/data/common/search/index.ts index 2ee0db384cf06..2ec4afbc60d96 100644 --- a/src/plugins/data/common/search/index.ts +++ b/src/plugins/data/common/search/index.ts @@ -23,3 +23,4 @@ export * from './expressions'; export * from './search_source'; export * from './tabify'; export * from './types'; +export * from './es_search'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 42c8864bb0bc0..57865f05871a1 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -379,7 +379,7 @@ export { export type { SearchSource } from './search'; -export { ISearchOptions } from '../common'; +export { ISearchOptions, isErrorResponse, isCompleteResponse, isPartialResponse } from '../common'; // Search namespace export const search = { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 91f7239401255..5919c1e294b2f 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1413,6 +1413,11 @@ export type InputTimeRange = TimeRange | { to: Moment; }; +// Warning: (ae-missing-release-tag) "isCompleteResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isCompleteResponse: (response?: IEsSearchResponse | undefined) => boolean | undefined; + // Warning: (ae-missing-release-tag) "ISearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1465,6 +1470,11 @@ export interface ISearchStartSearchSource { createEmpty: () => ISearchSource; } +// Warning: (ae-missing-release-tag) "isErrorResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isErrorResponse: (response?: IEsSearchResponse | undefined) => boolean | undefined; + // Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1475,6 +1485,11 @@ export const isFilter: (x: unknown) => x is Filter; // @public (undocumented) export const isFilters: (x: unknown) => x is Filter[]; +// Warning: (ae-missing-release-tag) "isPartialResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isPartialResponse: (response?: IEsSearchResponse | undefined) => boolean | undefined; + // Warning: (ae-missing-release-tag) "isQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index f7ae9fc6d0f91..c8fe72e6f2c1e 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -13,6 +13,7 @@ import { SearchInterceptorDeps, UI_SETTINGS, } from '../../../../../src/plugins/data/public'; +import { isErrorResponse, isCompleteResponse } from '../../../../../src/plugins/data/public'; import { AbortError, toPromise } from '../../../../../src/plugins/data/common'; import { IAsyncSearchOptions } from '.'; import { IAsyncSearchRequest, ENHANCED_ES_SEARCH_STRATEGY } from '../../common'; @@ -66,12 +67,12 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { return this.runSearch(request, combinedSignal, strategy).pipe( expand((response) => { // If the response indicates of an error, stop polling and complete the observable - if (!response || (!response.isRunning && response.isPartial)) { + if (isErrorResponse(response)) { return throwError(new AbortError()); } // If the response indicates it is complete, stop polling and complete the observable - if (!response.isRunning) { + if (isCompleteResponse(response)) { return EMPTY; } diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 72ea1f096e8fb..f3cf67a487a68 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -19,7 +19,11 @@ import { shimHitsTotal, } from '../../../../../src/plugins/data/server'; import { IEnhancedEsSearchRequest } from '../../common'; -import { ISearchOptions, IEsSearchResponse } from '../../../../../src/plugins/data/common/search'; +import { + ISearchOptions, + IEsSearchResponse, + isCompleteResponse, +} from '../../../../../src/plugins/data/common/search'; function isEnhancedEsSearchResponse(response: any): response is IEsSearchResponse { return response.hasOwnProperty('isPartial') && response.hasOwnProperty('isRunning'); @@ -48,8 +52,7 @@ export const enhancedEsSearchStrategyProvider = ( usage && isAsync && isEnhancedEsSearchResponse(response) && - !response.isRunning && - !response.isPartial + isCompleteResponse(response) ) { usage.trackSuccess(response.rawResponse.took); } diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts index d70762615818b..3d79c83dc42cb 100644 --- a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts @@ -18,7 +18,11 @@ import { LastTimeDetails, LastEventIndexKey, } from '../../../../../common/search_strategy/timeline'; -import { AbortError } from '../../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../../src/plugins/data/common'; import { useWithSource } from '../../source'; import * as i18n from './translations'; @@ -80,7 +84,7 @@ export const useTimelineLastEventTime = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setTimelineLastEventTimeResponse((prevResponse) => ({ @@ -91,7 +95,7 @@ export const useTimelineLastEventTime = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts index 65ad3cc994c67..8e0c133f95b4d 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts @@ -19,7 +19,11 @@ import { MatrixHistogramStrategyResponse, MatrixHistogramData, } from '../../../../common/search_strategy/security_solution'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isErrorResponse, + isCompleteResponse, +} from '../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import * as i18n from './translations'; @@ -90,7 +94,7 @@ export const useMatrixHistogram = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setMatrixHistogramResponse((prevResponse) => ({ @@ -102,7 +106,7 @@ export const useMatrixHistogram = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx index 34f2385051f4c..7bf4f7a833fb8 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx @@ -9,7 +9,11 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { shallowEqual, useSelector } from 'react-redux'; import deepEqual from 'fast-deep-equal'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { HostsQueries } from '../../../../common/search_strategy/security_solution'; @@ -136,7 +140,7 @@ export const useAuthentications = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setAuthenticationsResponse((prevResponse) => ({ @@ -149,7 +153,7 @@ export const useAuthentications = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx index 7b248d867bb76..f68c340a47723 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx @@ -21,7 +21,11 @@ import { } from '../../../../../common/search_strategy/security_solution/hosts'; import * as i18n from './translations'; -import { AbortError } from '../../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; @@ -93,7 +97,7 @@ export const useHostDetails = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setHostDetailsResponse((prevResponse) => ({ @@ -104,7 +108,7 @@ export const useHostDetails = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx index 169fe58e9a2cc..a6376642dfa29 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx @@ -18,7 +18,11 @@ import { import { useWithSource } from '../../../../common/containers/source'; import * as i18n from './translations'; -import { AbortError } from '../../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../../src/plugins/data/common'; const ID = 'firstLastSeenHostQuery'; @@ -72,7 +76,7 @@ export const useFirstLastSeenHost = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setFirstLastSeenHostResponse((prevResponse) => ({ @@ -83,7 +87,7 @@ export const useFirstLastSeenHost = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx index 11b853e0ebeb0..2eb926a9733c3 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx @@ -26,7 +26,11 @@ import { import { ESTermQuery } from '../../../../common/typed_json'; import * as i18n from './translations'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; @@ -133,7 +137,7 @@ export const useAllHost = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setHostsResponse((prevResponse) => ({ @@ -146,7 +150,7 @@ export const useAllHost = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx index 82f5a97e9e413..ae4ea83f88725 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx @@ -9,7 +9,11 @@ import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { PageInfoPaginated, UncommonProcessesEdges } from '../../../graphql/types'; @@ -135,7 +139,7 @@ export const useUncommonProcesses = ({ ) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setUncommonProcessesResponse((prevResponse) => ({ @@ -148,7 +152,7 @@ export const useUncommonProcesses = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/details/index.tsx b/x-pack/plugins/security_solution/public/network/containers/details/index.tsx index 597f85ff082e2..f6ea86bd552f4 100644 --- a/x-pack/plugins/security_solution/public/network/containers/details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/details/index.tsx @@ -19,7 +19,11 @@ import { NetworkDetailsRequestOptions, NetworkDetailsStrategyResponse, } from '../../../../common/search_strategy'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import * as i18n from './translations'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; @@ -88,7 +92,7 @@ export const useNetworkDetails = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkDetailsResponse((prevResponse) => ({ @@ -99,7 +103,7 @@ export const useNetworkDetails = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx index 295cbff76f6aa..2afbff3138c6b 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx @@ -20,7 +20,11 @@ import { import { ESTermQuery } from '../../../../../common/typed_json'; import * as i18n from './translations'; -import { AbortError } from '../../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; @@ -89,7 +93,7 @@ export const useNetworkKpiDns = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkKpiDnsResponse((prevResponse) => ({ @@ -100,7 +104,7 @@ export const useNetworkKpiDns = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx index 8ab94432746f4..26b57ef36b09d 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx @@ -20,7 +20,11 @@ import { import { ESTermQuery } from '../../../../../common/typed_json'; import * as i18n from './translations'; -import { AbortError } from '../../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; @@ -96,7 +100,7 @@ export const useNetworkKpiNetworkEvents = ({ ) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkKpiNetworkEventsResponse((prevResponse) => ({ @@ -107,7 +111,7 @@ export const useNetworkKpiNetworkEvents = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx index f7630352fc3c4..c97c1e43e699a 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx @@ -20,7 +20,11 @@ import { import { ESTermQuery } from '../../../../../common/typed_json'; import * as i18n from './translations'; -import { AbortError } from '../../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; @@ -96,7 +100,7 @@ export const useNetworkKpiTlsHandshakes = ({ ) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkKpiTlsHandshakesResponse((prevResponse) => ({ @@ -107,7 +111,7 @@ export const useNetworkKpiTlsHandshakes = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx index 5f1bd782b9abd..4e8b4ad38b711 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx @@ -20,7 +20,11 @@ import { import { ESTermQuery } from '../../../../../common/typed_json'; import * as i18n from './translations'; -import { AbortError } from '../../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; @@ -96,7 +100,7 @@ export const useNetworkKpiUniqueFlows = ({ ) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkKpiUniqueFlowsResponse((prevResponse) => ({ @@ -107,7 +111,7 @@ export const useNetworkKpiUniqueFlows = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx index f32f43d811137..b518f95212129 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx @@ -21,7 +21,11 @@ import { import { ESTermQuery } from '../../../../../common/typed_json'; import * as i18n from './translations'; -import { AbortError } from '../../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; @@ -103,7 +107,7 @@ export const useNetworkKpiUniquePrivateIps = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkKpiUniquePrivateIpsResponse((prevResponse) => ({ @@ -118,7 +122,7 @@ export const useNetworkKpiUniquePrivateIps = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx index 6f48cba2ebda1..209f8da0d8fae 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx @@ -23,7 +23,11 @@ import { NetworkDnsStrategyResponse, MatrixOverOrdinalHistogramData, } from '../../../../common/search_strategy/security_solution/network'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import * as i18n from './translations'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; @@ -130,7 +134,7 @@ export const useNetworkDns = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkDnsResponse((prevResponse) => ({ @@ -144,7 +148,7 @@ export const useNetworkDns = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx index d3e8067d1802e..9244d571bb67b 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx @@ -23,7 +23,11 @@ import { NetworkHttpStrategyResponse, SortField, } from '../../../../common/search_strategy'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import * as i18n from './translations'; import { InspectResponse } from '../../../types'; import { getInspectResponse } from '../../../helpers'; @@ -130,7 +134,7 @@ export const useNetworkHttp = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkHttpResponse((prevResponse) => ({ @@ -143,7 +147,7 @@ export const useNetworkHttp = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx index 747f5e4f502dd..8138d30f2c510 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx @@ -24,7 +24,11 @@ import { NetworkTopCountriesStrategyResponse, PageInfoPaginated, } from '../../../../common/search_strategy'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import * as i18n from './translations'; @@ -129,7 +133,7 @@ export const useNetworkTopCountries = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkTopCountriesResponse((prevResponse) => ({ @@ -142,7 +146,7 @@ export const useNetworkTopCountries = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx index cc0da816c57ec..76c2ae2871a38 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx @@ -24,7 +24,11 @@ import { NetworkTopNFlowStrategyResponse, PageInfoPaginated, } from '../../../../common/search_strategy'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import * as i18n from './translations'; @@ -127,7 +131,7 @@ export const useNetworkTopNFlow = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkTopNFlowResponse((prevResponse) => ({ @@ -140,7 +144,7 @@ export const useNetworkTopNFlow = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx index df02acf208603..f9393cfc26692 100644 --- a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx @@ -22,7 +22,11 @@ import { NetworkTlsRequestOptions, NetworkTlsStrategyResponse, } from '../../../../common/search_strategy/security_solution/network'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import * as i18n from './translations'; import { getInspectResponse } from '../../../helpers'; @@ -129,7 +133,7 @@ export const useNetworkTls = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkTlsResponse((prevResponse) => ({ @@ -142,7 +146,7 @@ export const useNetworkTls = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/users/index.tsx b/x-pack/plugins/security_solution/public/network/containers/users/index.tsx index 608ccdb084709..a289f8d16e9b2 100644 --- a/x-pack/plugins/security_solution/public/network/containers/users/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/users/index.tsx @@ -23,7 +23,11 @@ import { NetworkUsersRequestOptions, NetworkUsersStrategyResponse, } from '../../../../common/search_strategy/security_solution/network'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import * as i18n from './translations'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; @@ -126,7 +130,7 @@ export const useNetworkUsers = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkUsersResponse((prevResponse) => ({ @@ -139,7 +143,7 @@ export const useNetworkUsers = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx index e011e6c7b6b65..75ab85fe0c429 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx @@ -20,7 +20,11 @@ import { createFilter } from '../../../common/containers/helpers'; import { ESQuery } from '../../../../common/typed_json'; import { useManageSource } from '../../../common/containers/sourcerer'; import { SOURCERER_FEATURE_FLAG_ON } from '../../../common/containers/sourcerer/constants'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import * as i18n from './translations'; @@ -96,7 +100,7 @@ export const useHostOverview = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setHostOverviewResponse((prevResponse) => ({ @@ -107,7 +111,7 @@ export const useHostOverview = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx index c61606e0c31dd..ae1fe942d8403 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx @@ -18,7 +18,11 @@ import { useKibana } from '../../../common/lib/kibana'; import { inputsModel } from '../../../common/store/inputs'; import { createFilter } from '../../../common/containers/helpers'; import { ESQuery } from '../../../../common/typed_json'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import * as i18n from './translations'; @@ -87,7 +91,7 @@ export const useNetworkOverview = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkOverviewResponse((prevResponse) => ({ @@ -98,7 +102,7 @@ export const useNetworkOverview = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx index af99c75ae701a..23c05805a5aa4 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx @@ -18,6 +18,7 @@ import { TimelineEventsDetailsRequestOptions, TimelineEventsDetailsStrategyResponse, } from '../../../../common/search_strategy'; +import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/public'; export interface EventsArgs { detailsData: TimelineEventsDetailsItem[] | null; } @@ -66,13 +67,13 @@ export const useTimelineEventsDetails = ({ ) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setTimelineDetailsResponse(response.data || []); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index f340096c75f2b..d56a601fda4a3 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -10,7 +10,11 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { ESQuery } from '../../../common/typed_json'; -import { IIndexPattern } from '../../../../../../src/plugins/data/public'; +import { + IIndexPattern, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../src/plugins/data/public'; import { DEFAULT_INDEX_KEY } from '../../../common/constants'; import { inputsModel } from '../../common/store'; @@ -167,7 +171,7 @@ export const useTimelineEvents = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setTimelineResponse((prevResponse) => ({ @@ -181,7 +185,7 @@ export const useTimelineEvents = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } From b3f605baed8722404873efd3122467cae08a676d Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 23 Sep 2020 09:06:03 +0200 Subject: [PATCH 31/92] [CSM] fix ingest data retry order messed up (#78163) --- x-pack/plugins/apm/e2e/ingest-data/replay.js | 55 +++++++++++--------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/apm/e2e/ingest-data/replay.js b/x-pack/plugins/apm/e2e/ingest-data/replay.js index 6bab95635f558..74c86b1b09ab4 100644 --- a/x-pack/plugins/apm/e2e/ingest-data/replay.js +++ b/x-pack/plugins/apm/e2e/ingest-data/replay.js @@ -70,34 +70,40 @@ function incrementSpinnerCount({ success }) { } let iterIndex = 0; +function setItemMetaAndHeaders(item) { + const headers = { + 'content-type': 'application/x-ndjson', + }; + + if (SECRET_TOKEN) { + headers.Authorization = `Bearer ${SECRET_TOKEN}`; + } + + if (item.url === '/intake/v2/rum/events') { + if (iterIndex === userAgents.length) { + // set some event agent to opbean + setRumAgent(item); + iterIndex = 0; + } + headers['User-Agent'] = userAgents[iterIndex]; + headers['X-Forwarded-For'] = userIps[iterIndex]; + iterIndex++; + } + return headers; +} + function setRumAgent(item) { - item.body = item.body.replace( - '"name":"client"', - '"name":"opbean-client-rum"' - ); + if (item.body) { + item.body = item.body.replace( + '"name":"client"', + '"name":"opbean-client-rum"' + ); + } } -async function insertItem(item) { +async function insertItem(item, headers) { try { const url = `${APM_SERVER_URL}${item.url}`; - const headers = { - 'content-type': 'application/x-ndjson', - }; - - if (item.url === '/intake/v2/rum/events') { - if (iterIndex === userAgents.length) { - // set some event agent to opbean - setRumAgent(item); - iterIndex = 0; - } - headers['User-Agent'] = userAgents[iterIndex]; - headers['X-Forwarded-For'] = userIps[iterIndex]; - iterIndex++; - } - - if (SECRET_TOKEN) { - headers.Authorization = `Bearer ${SECRET_TOKEN}`; - } await axios({ method: item.method, @@ -133,8 +139,9 @@ async function init() { await Promise.all( items.map(async (item) => { try { + const headers = setItemMetaAndHeaders(item); // retry 5 times with exponential backoff - await pRetry(() => limit(() => insertItem(item)), { + await pRetry(() => limit(() => insertItem(item, headers)), { retries: 5, }); incrementSpinnerCount({ success: true }); From 62c095b258fdc01326092016dc5f28d28ecbe679 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 23 Sep 2020 10:48:55 +0300 Subject: [PATCH 32/92] 'Auto' interval must be correctly calculated for natural numbers (#77995) * 'Auto' interval must be correctly calculated for natural numbers * fix ts error * fix PR comments Co-authored-by: Elastic Machine --- .../common/search/aggs/buckets/histogram.ts | 1 + .../lib/histogram_calculate_interval.test.ts | 93 ++++++++++++++++++- .../lib/histogram_calculate_interval.ts | 27 +++++- 3 files changed, 117 insertions(+), 4 deletions(-) diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.ts b/src/plugins/data/common/search/aggs/buckets/histogram.ts index 4b631e1fd7cd7..c3d3f041dd0c7 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.ts @@ -158,6 +158,7 @@ export const getHistogramBucketAgg = ({ maxBucketsUiSettings: getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS), maxBucketsUserInput: aggConfig.params.maxBars, intervalBase: aggConfig.params.intervalBase, + esTypes: aggConfig.params.field?.spec?.esTypes || [], }); }, }, diff --git a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts index d3a95b32cd425..7e5e20e5917aa 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts @@ -21,6 +21,7 @@ import { calculateHistogramInterval, CalculateHistogramIntervalParams, } from './histogram_calculate_interval'; +import { ES_FIELD_TYPES } from '../../../../types'; describe('calculateHistogramInterval', () => { describe('auto calculating mode', () => { @@ -36,10 +37,91 @@ describe('calculateHistogramInterval', () => { min: 0, max: 1, }, + esTypes: [], }; }); describe('maxBucketsUserInput is defined', () => { + test('should set 1 as an interval for integer numbers that are less than maxBuckets #1', () => { + const p = { + ...params, + maxBucketsUserInput: 100, + values: { + min: 1, + max: 50, + }, + esTypes: [ES_FIELD_TYPES.INTEGER], + }; + expect(calculateHistogramInterval(p)).toEqual(1); + }); + + test('should set 1 as an interval for integer numbers that are less than maxBuckets #2', () => { + const p = { + ...params, + maxBucketsUiSettings: 1000, + maxBucketsUserInput: 258, + values: { + min: 521, + max: 689, + }, + esTypes: [ES_FIELD_TYPES.INTEGER], + }; + expect(calculateHistogramInterval(p)).toEqual(1); + }); + + test('should set correct interval for integer numbers that are greater than maxBuckets #1', () => { + const p = { + ...params, + maxBucketsUserInput: 100, + values: { + min: 400, + max: 790, + }, + esTypes: [ES_FIELD_TYPES.INTEGER, ES_FIELD_TYPES.SHORT], + }; + expect(calculateHistogramInterval(p)).toEqual(5); + }); + + test('should set correct interval for integer numbers that are greater than maxBuckets #2', () => { + // diff === 3456211; interval === 50000; buckets === 69 + const p = { + ...params, + maxBucketsUserInput: 100, + values: { + min: 567, + max: 3456778, + }, + esTypes: [ES_FIELD_TYPES.LONG], + }; + expect(calculateHistogramInterval(p)).toEqual(50000); + }); + + test('should not set integer interval if the field type is float #1', () => { + const p = { + ...params, + maxBucketsUserInput: 100, + values: { + min: 0, + max: 1, + }, + esTypes: [ES_FIELD_TYPES.FLOAT], + }; + expect(calculateHistogramInterval(p)).toEqual(0.01); + }); + + test('should not set integer interval if the field type is float #2', () => { + const p = { + ...params, + maxBucketsUserInput: 100, + values: { + min: 0, + max: 1, + }, + esTypes: [ES_FIELD_TYPES.INTEGER, ES_FIELD_TYPES.FLOAT], + }; + expect(calculateHistogramInterval(p)).toEqual(0.01); + }); + test('should not set interval which more than largest possible', () => { const p = { ...params, @@ -48,6 +130,7 @@ describe('calculateHistogramInterval', () => { min: 150, max: 250, }, + esTypes: [ES_FIELD_TYPES.SHORT], }; expect(calculateHistogramInterval(p)).toEqual(1); }); @@ -61,6 +144,7 @@ describe('calculateHistogramInterval', () => { min: 0.1, max: 0.9, }, + esTypes: [ES_FIELD_TYPES.FLOAT], }) ).toBe(0.02); }); @@ -74,6 +158,7 @@ describe('calculateHistogramInterval', () => { min: 10.45, max: 1000.05, }, + esTypes: [ES_FIELD_TYPES.FLOAT], }) ).toBe(100); }); @@ -88,6 +173,7 @@ describe('calculateHistogramInterval', () => { min: 0, max: 100, }, + esTypes: [ES_FIELD_TYPES.BYTE], }) ).toEqual(1); }); @@ -100,8 +186,9 @@ describe('calculateHistogramInterval', () => { min: 1, max: 10, }, + esTypes: [ES_FIELD_TYPES.INTEGER], }) - ).toEqual(0.1); + ).toEqual(1); }); test('should set intervals for integer numbers (diff more than maxBucketsUiSettings)', () => { @@ -113,6 +200,7 @@ describe('calculateHistogramInterval', () => { min: 45678, max: 90123, }, + esTypes: [ES_FIELD_TYPES.INTEGER], }) ).toEqual(500); }); @@ -127,6 +215,7 @@ describe('calculateHistogramInterval', () => { min: 1.245, max: 2.9, }, + esTypes: [ES_FIELD_TYPES.FLOAT], }) ).toEqual(0.02); expect( @@ -136,6 +225,7 @@ describe('calculateHistogramInterval', () => { min: 0.5, max: 2.3, }, + esTypes: [ES_FIELD_TYPES.FLOAT], }) ).toEqual(0.02); }); @@ -149,6 +239,7 @@ describe('calculateHistogramInterval', () => { min: 0.1, max: 0.9, }, + esTypes: [ES_FIELD_TYPES.FLOAT], }) ).toBe(0.01); }); diff --git a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts index 378340e876296..313ecf1000f41 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts @@ -18,6 +18,7 @@ */ import { isAutoInterval } from '../_interval_options'; +import { ES_FIELD_TYPES } from '../../../../types'; interface IntervalValuesRange { min: number; @@ -28,6 +29,7 @@ export interface CalculateHistogramIntervalParams { interval: string; maxBucketsUiSettings: number; maxBucketsUserInput?: number | ''; + esTypes: ES_FIELD_TYPES[]; intervalBase?: number; values?: IntervalValuesRange; } @@ -77,11 +79,27 @@ const calculateForGivenInterval = ( - The lower power of 10, times 2 - The lower power of 10, times 5 **/ -const calculateAutoInterval = (diff: number, maxBars: number) => { +const calculateAutoInterval = (diff: number, maxBars: number, esTypes: ES_FIELD_TYPES[]) => { const exactInterval = diff / maxBars; - const lowerPower = Math.pow(10, Math.floor(Math.log10(exactInterval))); + // For integer fields that are less than maxBars, we should use 1 as the value of interval + // Elastic has 4 integer data types: long, integer, short, byte + // see: https://www.elastic.co/guide/en/elasticsearch/reference/current/number.html + if ( + diff < maxBars && + esTypes.every((esType) => + [ + ES_FIELD_TYPES.INTEGER, + ES_FIELD_TYPES.LONG, + ES_FIELD_TYPES.SHORT, + ES_FIELD_TYPES.BYTE, + ].includes(esType) + ) + ) { + return 1; + } + const lowerPower = Math.pow(10, Math.floor(Math.log10(exactInterval))); const autoBuckets = diff / lowerPower; if (autoBuckets > maxBars) { @@ -103,6 +121,7 @@ export const calculateHistogramInterval = ({ maxBucketsUserInput, intervalBase, values, + esTypes, }: CalculateHistogramIntervalParams) => { const isAuto = isAutoInterval(interval); let calculatedInterval = isAuto ? 0 : parseFloat(interval); @@ -119,8 +138,10 @@ export const calculateHistogramInterval = ({ calculatedInterval = isAuto ? calculateAutoInterval( diff, + // Mind maxBucketsUserInput can be an empty string, hence we need to ensure it here - Math.min(maxBucketsUiSettings, maxBucketsUserInput || maxBucketsUiSettings) + Math.min(maxBucketsUiSettings, maxBucketsUserInput || maxBucketsUiSettings), + esTypes ) : calculateForGivenInterval(diff, calculatedInterval, maxBucketsUiSettings); } From 0d09cea436c9a12eb100d238fd5d53874b51acdf Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Wed, 23 Sep 2020 09:52:51 +0200 Subject: [PATCH 33/92] Remove legacy plugins support (#77599) * remove ALL the things. * adapt some types and tests * restore ensureValidConfiguration * fix legacy service tests * adapt uiRender mixin * remove legacy types * update generated doc * restore legacy plugin schema * update generated doc * remove remaining code of x-pack/legacy * adapt imports due to merge * cleanup CODEOWNERS * cleanup gitignore & i18nrc * cleanup tsconfig.json * remove unused i18n keys * add back `"legacy/plugins/**/*",` to tsconfig until legacy space plugin is deleted * fix create_jest_config * remove references from eslintrc * more eslint cleanup * remove `x-pack/index.js` * fix xpack gulp scripts * fix bug with default + named imports from boom * remove rules from eslintrc * remove LegacyInternals * review comments * update generated doc * cleanup legacy metadatas * revert changes to eslintrc * update generated doc --- .eslintrc.js | 64 +-- .github/CODEOWNERS | 13 +- kibana.d.ts | 5 - src/cli/serve/serve.js | 16 +- .../injected_metadata_service.ts | 25 - src/core/server/index.ts | 9 +- .../config/ensure_valid_configuration.test.ts | 7 +- .../config/ensure_valid_configuration.ts | 8 +- .../config/get_unused_config_keys.test.ts | 48 +- .../legacy/config/get_unused_config_keys.ts | 12 +- src/core/server/legacy/index.ts | 2 - .../server/legacy/legacy_internals.test.ts | 211 -------- src/core/server/legacy/legacy_internals.ts | 93 ---- src/core/server/legacy/legacy_service.mock.ts | 18 +- .../legacy/legacy_service.test.mocks.ts | 40 -- src/core/server/legacy/legacy_service.test.ts | 131 ++--- src/core/server/legacy/legacy_service.ts | 136 ++--- .../legacy/plugins/collect_ui_exports.js | 21 - .../plugins/find_legacy_plugin_specs.ts | 135 ----- src/core/server/legacy/plugins/index.ts | 21 - .../log_legacy_plugins_warning.test.ts | 89 ---- .../plugins/log_legacy_plugins_warning.ts | 53 -- src/core/server/legacy/types.ts | 129 +---- src/core/server/rendering/__mocks__/params.ts | 3 - .../rendering/__mocks__/rendering_service.ts | 2 - .../rendering_service.test.ts.snap | 50 -- .../rendering/rendering_service.test.ts | 13 - .../server/rendering/rendering_service.tsx | 29 +- src/core/server/rendering/types.ts | 20 - src/core/server/server.api.md | 55 -- src/core/server/server.ts | 10 +- src/dev/jest/config.js | 1 - src/legacy/plugin_discovery/README.md | 148 ------ .../__tests__/find_plugin_specs.js | 219 -------- .../__tests__/fixtures/conflicts/foo/index.js | 27 - .../fixtures/conflicts/foo/package.json | 4 - .../__tests__/fixtures/plugins/bar/index.js | 29 - .../fixtures/plugins/bar/package.json | 4 - .../fixtures/plugins/broken/index.js | 22 - .../fixtures/plugins/broken/package1.json | 4 - .../__tests__/fixtures/plugins/foo/index.js | 24 - .../fixtures/plugins/foo/package.json | 4 - src/legacy/plugin_discovery/errors.js | 84 --- .../plugin_discovery/find_plugin_specs.js | 234 --------- src/legacy/plugin_discovery/index.js | 22 - .../__tests__/extend_config_service.js | 162 ------ .../plugin_config/__tests__/schema.js | 92 ---- .../plugin_config/__tests__/settings.js | 61 --- .../plugin_config/extend_config_service.js | 50 -- .../plugin_discovery/plugin_config/index.js | 20 - .../plugin_discovery/plugin_config/schema.js | 46 -- .../plugin_config/settings.js | 34 -- .../__tests__/reduce_export_specs.js | 75 --- .../plugin_discovery/plugin_exports/index.js | 20 - .../plugin_exports/reduce_export_specs.js | 47 -- .../plugin_pack/__tests__/create_pack.js | 85 --- .../fixtures/plugins/broken/package.json | 3 - .../fixtures/plugins/broken_code/index.js | 7 - .../fixtures/plugins/broken_code/package.json | 4 - .../fixtures/plugins/exports_number/index.js | 20 - .../plugins/exports_number/package.json | 4 - .../fixtures/plugins/exports_object/index.js | 22 - .../plugins/exports_object/package.json | 4 - .../fixtures/plugins/exports_string/index.js | 20 - .../plugins/exports_string/package.json | 4 - .../__tests__/fixtures/plugins/foo/index.js | 24 - .../fixtures/plugins/foo/package.json | 4 - .../__tests__/fixtures/plugins/index.js | 20 - .../__tests__/fixtures/plugins/lib/index.js | 20 - .../__tests__/fixtures/plugins/lib/my_lib.js | 22 - .../fixtures/plugins/prebuilt/index.js | 14 - .../fixtures/plugins/prebuilt/package.json | 3 - .../__tests__/package_json_at_path.js | 88 ---- .../__tests__/package_jsons_in_directory.js | 83 --- .../plugin_pack/__tests__/plugin_pack.js | 126 ----- .../plugin_pack/__tests__/utils.js | 37 -- .../plugin_pack/create_pack.js | 54 -- .../plugin_discovery/plugin_pack/index.js | 23 - .../plugin_discovery/plugin_pack/lib/fs.js | 85 --- .../plugin_discovery/plugin_pack/lib/index.js | 20 - .../plugin_pack/package_json_at_path.js | 62 --- .../plugin_pack/package_jsons_in_directory.js | 52 -- .../plugin_pack/plugin_pack.js | 74 --- .../__tests__/is_version_compatible.js | 48 -- .../plugin_spec/__tests__/plugin_spec.js | 496 ------------------ .../plugin_discovery/plugin_spec/index.js | 20 - .../plugin_spec/is_version_compatible.js | 30 -- .../plugin_spec/plugin_spec.js | 210 -------- .../plugin_spec/plugin_spec_options.d.ts | 35 -- src/legacy/plugin_discovery/types.ts | 107 ---- src/legacy/server/config/schema.js | 68 +-- src/legacy/server/kbn_server.d.ts | 20 +- src/legacy/server/kbn_server.js | 21 +- src/legacy/server/plugins/index.js | 22 - src/legacy/server/plugins/initialize_mixin.js | 47 -- .../server/plugins/lib/call_plugin_hook.js | 50 -- .../plugins/lib/call_plugin_hook.test.js | 101 ---- src/legacy/server/plugins/lib/index.js | 21 - src/legacy/server/plugins/lib/plugin.js | 114 ---- src/legacy/server/plugins/scan_mixin.js | 23 - .../server/plugins/wait_for_plugins_init.js | 53 -- src/legacy/types.ts | 20 - .../fixtures/plugin_async_foo/index.js | 40 -- .../fixtures/plugin_async_foo/package.json | 4 - .../ui/__tests__/fixtures/plugin_bar/index.js | 36 -- .../fixtures/plugin_bar/package.json | 4 - .../ui/__tests__/fixtures/plugin_foo/index.js | 36 -- .../fixtures/plugin_foo/package.json | 4 - .../ui/__tests__/fixtures/test_app/index.js | 39 -- .../__tests__/fixtures/test_app/package.json | 4 - src/legacy/ui/index.js | 1 - src/legacy/ui/ui_exports/README.md | 95 ---- .../ui/ui_exports/collect_ui_exports.ts | 31 -- src/legacy/ui/ui_exports/index.js | 20 - .../ui/ui_exports/ui_export_defaults.js | 20 - .../ui/ui_exports/ui_export_types/index.js | 36 -- .../ui_export_types/modify_injected_vars.js | 32 -- .../ui_export_types/modify_reduce/alias.js | 28 - .../ui_export_types/modify_reduce/debug.js | 31 -- .../ui_export_types/modify_reduce/index.js | 24 - .../ui_export_types/modify_reduce/map_spec.js | 29 - .../modify_reduce/unique_keys.js | 32 -- .../ui_export_types/modify_reduce/wrap.js | 45 -- .../reduce/flat_concat_at_type.js | 28 - .../reduce/flat_concat_values_at_type.js | 30 -- .../ui_export_types/reduce/index.js | 22 - .../reduce/lib/create_type_reducer.js | 32 -- .../ui_export_types/reduce/lib/flat_concat.js | 27 - .../ui_export_types/reduce/lib/index.js | 22 - .../ui_export_types/reduce/lib/merge_with.js | 38 -- .../ui_export_types/reduce/merge_at_type.js | 25 - .../ui_export_types/saved_object.js | 65 --- .../ui_export_types/task_definitions.js | 24 - .../ui_export_types/ui_nav_links.js | 24 - .../ui_exports/ui_export_types/ui_settings.js | 23 - .../ui/ui_exports/ui_export_types/unknown.js | 23 - src/legacy/ui/ui_render/ui_render_mixin.js | 217 ++++---- x-pack/.gitignore | 4 - x-pack/.i18nrc.json | 12 +- x-pack/dev-tools/jest/create_jest_config.js | 6 +- x-pack/index.js | 11 - x-pack/legacy/common/__tests__/poller.js | 240 --------- x-pack/legacy/common/constants/index.ts | 23 - .../legacy/common/constants/license_status.ts | 10 - .../legacy/common/constants/license_types.ts | 31 -- x-pack/legacy/common/eui_draggable/index.d.ts | 17 - .../common/eui_styled_components/index.ts | 20 - x-pack/legacy/common/poller.d.ts | 14 - x-pack/legacy/common/poller.js | 79 --- x-pack/legacy/plugins/xpack_main/index.js | 31 -- .../server/lib/__tests__/setup_xpack_main.js | 68 --- .../server/lib/__tests__/xpack_info.js | 398 -------------- .../xpack_main/server/lib/setup_xpack_main.js | 33 -- .../xpack_main/server/lib/xpack_info.ts | 240 --------- .../server/lib/xpack_info_license.test.js | 207 -------- .../server/lib/xpack_info_license.ts | 111 ---- .../routes/api/v1/__tests__/xpack_info.js | 85 --- .../server/routes/api/v1/xpack_info.js | 25 - .../plugins/xpack_main/server/xpack_main.d.ts | 14 - .../lib/__tests__/key_case_converter.js | 117 ----- .../server/lib/__tests__/kibana_state.js | 129 ----- .../server/lib/check_license/check_license.js | 75 --- .../lib/check_license/check_license.test.js | 132 ----- .../legacy/server/lib/check_license/index.js | 7 - x-pack/legacy/server/lib/constants/index.ts | 7 - .../legacy/server/lib/constants/xpack_info.ts | 7 - .../call_with_request_factory.js | 12 - .../call_with_request_factory/index.d.ts | 18 - .../call_with_request_factory/index.js | 7 - .../__tests__/wrap_custom_error.js | 21 - .../error_wrappers/__tests__/wrap_es_error.js | 39 -- .../__tests__/wrap_unknown_error.js | 19 - .../create_router/error_wrappers/index.d.ts | 12 - .../lib/create_router/error_wrappers/index.js | 9 - .../error_wrappers/wrap_custom_error.js | 18 - .../error_wrappers/wrap_es_error.js | 59 --- .../error_wrappers/wrap_unknown_error.js | 17 - .../server/lib/create_router/index.d.ts | 38 -- .../legacy/server/lib/create_router/index.js | 61 --- .../__tests__/is_es_error_factory.js | 44 -- .../is_es_error_factory/index.js | 7 - .../is_es_error_factory.js | 18 - .../__tests__/license_pre_routing_factory.js | 70 --- .../license_pre_routing_factory/index.js | 7 - .../license_pre_routing_factory.js | 26 - .../legacy/server/lib/key_case_converter.js | 52 -- .../legacy/server/lib/parse_kibana_state.js | 55 -- .../lib/register_license_checker/index.d.ts | 15 - .../lib/register_license_checker/index.js | 7 - .../register_license_checker.js | 34 -- .../watch_status_and_license_to_initialize.js | 83 --- ...h_status_and_license_to_initialize.test.js | 301 ----------- x-pack/legacy/server/lib/xpack_usage.js | 16 - .../actions/server/constants/plugin.ts | 4 +- .../plugins/alerts/server/constants/plugin.ts | 4 +- .../apm/common/service_health_status.ts | 2 +- .../metric_control/custom_metric_form.tsx | 5 +- .../metric_control/metrics_edit_mode.tsx | 5 +- .../waffle/metric_control/mode_switcher.tsx | 5 +- .../applications/ingest_manager/index.tsx | 2 +- .../public/application/index.tsx | 2 +- .../observability/public/hooks/use_theme.tsx | 2 +- .../mock/endpoint/app_root_provider.tsx | 2 +- .../pages/policy/view/vertical_divider.ts | 2 +- x-pack/plugins/transform/common/constants.ts | 4 +- .../server/routes/api/error_utils.ts | 59 ++- .../server/routes/api/field_histograms.ts | 4 +- .../transform/server/routes/api/transforms.ts | 3 +- .../routes/api/transforms_audit_messages.ts | 3 +- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - .../watcher/common/constants/plugin.ts | 4 +- x-pack/plugins/watcher/server/types.ts | 3 - .../common}/eui_styled_components.tsx | 0 .../xpack_legacy/common/index.ts} | 2 +- x-pack/tasks/build.ts | 3 - x-pack/tasks/helpers/flags.ts | 21 - x-pack/tsconfig.json | 4 - x-pack/typings/hapi.d.ts | 2 - 219 files changed, 295 insertions(+), 9658 deletions(-) delete mode 100644 src/core/server/legacy/legacy_internals.test.ts delete mode 100644 src/core/server/legacy/legacy_internals.ts delete mode 100644 src/core/server/legacy/legacy_service.test.mocks.ts delete mode 100644 src/core/server/legacy/plugins/collect_ui_exports.js delete mode 100644 src/core/server/legacy/plugins/find_legacy_plugin_specs.ts delete mode 100644 src/core/server/legacy/plugins/index.ts delete mode 100644 src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts delete mode 100644 src/core/server/legacy/plugins/log_legacy_plugins_warning.ts delete mode 100644 src/legacy/plugin_discovery/README.md delete mode 100644 src/legacy/plugin_discovery/__tests__/find_plugin_specs.js delete mode 100644 src/legacy/plugin_discovery/__tests__/fixtures/conflicts/foo/index.js delete mode 100644 src/legacy/plugin_discovery/__tests__/fixtures/conflicts/foo/package.json delete mode 100644 src/legacy/plugin_discovery/__tests__/fixtures/plugins/bar/index.js delete mode 100644 src/legacy/plugin_discovery/__tests__/fixtures/plugins/bar/package.json delete mode 100644 src/legacy/plugin_discovery/__tests__/fixtures/plugins/broken/index.js delete mode 100644 src/legacy/plugin_discovery/__tests__/fixtures/plugins/broken/package1.json delete mode 100644 src/legacy/plugin_discovery/__tests__/fixtures/plugins/foo/index.js delete mode 100644 src/legacy/plugin_discovery/__tests__/fixtures/plugins/foo/package.json delete mode 100644 src/legacy/plugin_discovery/errors.js delete mode 100644 src/legacy/plugin_discovery/find_plugin_specs.js delete mode 100644 src/legacy/plugin_discovery/index.js delete mode 100644 src/legacy/plugin_discovery/plugin_config/__tests__/extend_config_service.js delete mode 100644 src/legacy/plugin_discovery/plugin_config/__tests__/schema.js delete mode 100644 src/legacy/plugin_discovery/plugin_config/__tests__/settings.js delete mode 100644 src/legacy/plugin_discovery/plugin_config/extend_config_service.js delete mode 100644 src/legacy/plugin_discovery/plugin_config/index.js delete mode 100644 src/legacy/plugin_discovery/plugin_config/schema.js delete mode 100644 src/legacy/plugin_discovery/plugin_config/settings.js delete mode 100644 src/legacy/plugin_discovery/plugin_exports/__tests__/reduce_export_specs.js delete mode 100644 src/legacy/plugin_discovery/plugin_exports/index.js delete mode 100644 src/legacy/plugin_discovery/plugin_exports/reduce_export_specs.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/create_pack.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken/package.json delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken_code/index.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken_code/package.json delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/index.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/package.json delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/index.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/package.json delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/index.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/package.json delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/index.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/package.json delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/index.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/index.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/my_lib.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/index.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/package.json delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/package_json_at_path.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/package_jsons_in_directory.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/plugin_pack.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/__tests__/utils.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/create_pack.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/index.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/lib/fs.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/lib/index.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/package_json_at_path.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/package_jsons_in_directory.js delete mode 100644 src/legacy/plugin_discovery/plugin_pack/plugin_pack.js delete mode 100644 src/legacy/plugin_discovery/plugin_spec/__tests__/is_version_compatible.js delete mode 100644 src/legacy/plugin_discovery/plugin_spec/__tests__/plugin_spec.js delete mode 100644 src/legacy/plugin_discovery/plugin_spec/index.js delete mode 100644 src/legacy/plugin_discovery/plugin_spec/is_version_compatible.js delete mode 100644 src/legacy/plugin_discovery/plugin_spec/plugin_spec.js delete mode 100644 src/legacy/plugin_discovery/plugin_spec/plugin_spec_options.d.ts delete mode 100644 src/legacy/plugin_discovery/types.ts delete mode 100644 src/legacy/server/plugins/index.js delete mode 100644 src/legacy/server/plugins/initialize_mixin.js delete mode 100644 src/legacy/server/plugins/lib/call_plugin_hook.js delete mode 100644 src/legacy/server/plugins/lib/call_plugin_hook.test.js delete mode 100644 src/legacy/server/plugins/lib/index.js delete mode 100644 src/legacy/server/plugins/lib/plugin.js delete mode 100644 src/legacy/server/plugins/scan_mixin.js delete mode 100644 src/legacy/server/plugins/wait_for_plugins_init.js delete mode 100644 src/legacy/types.ts delete mode 100644 src/legacy/ui/__tests__/fixtures/plugin_async_foo/index.js delete mode 100644 src/legacy/ui/__tests__/fixtures/plugin_async_foo/package.json delete mode 100644 src/legacy/ui/__tests__/fixtures/plugin_bar/index.js delete mode 100644 src/legacy/ui/__tests__/fixtures/plugin_bar/package.json delete mode 100644 src/legacy/ui/__tests__/fixtures/plugin_foo/index.js delete mode 100644 src/legacy/ui/__tests__/fixtures/plugin_foo/package.json delete mode 100644 src/legacy/ui/__tests__/fixtures/test_app/index.js delete mode 100644 src/legacy/ui/__tests__/fixtures/test_app/package.json delete mode 100644 src/legacy/ui/ui_exports/README.md delete mode 100644 src/legacy/ui/ui_exports/collect_ui_exports.ts delete mode 100644 src/legacy/ui/ui_exports/index.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_defaults.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/index.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/modify_injected_vars.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/modify_reduce/alias.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/modify_reduce/debug.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/modify_reduce/index.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/modify_reduce/map_spec.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/modify_reduce/unique_keys.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/modify_reduce/wrap.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/reduce/flat_concat_at_type.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/reduce/flat_concat_values_at_type.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/reduce/index.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/reduce/lib/create_type_reducer.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/reduce/lib/flat_concat.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/reduce/lib/index.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/reduce/lib/merge_with.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/reduce/merge_at_type.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/saved_object.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/task_definitions.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/ui_nav_links.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/ui_settings.js delete mode 100644 src/legacy/ui/ui_exports/ui_export_types/unknown.js delete mode 100644 x-pack/index.js delete mode 100644 x-pack/legacy/common/__tests__/poller.js delete mode 100644 x-pack/legacy/common/constants/index.ts delete mode 100644 x-pack/legacy/common/constants/license_status.ts delete mode 100644 x-pack/legacy/common/constants/license_types.ts delete mode 100644 x-pack/legacy/common/eui_draggable/index.d.ts delete mode 100644 x-pack/legacy/common/eui_styled_components/index.ts delete mode 100644 x-pack/legacy/common/poller.d.ts delete mode 100644 x-pack/legacy/common/poller.js delete mode 100644 x-pack/legacy/plugins/xpack_main/index.js delete mode 100644 x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js delete mode 100644 x-pack/legacy/plugins/xpack_main/server/lib/__tests__/xpack_info.js delete mode 100644 x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js delete mode 100644 x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.ts delete mode 100644 x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.test.js delete mode 100644 x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.ts delete mode 100644 x-pack/legacy/plugins/xpack_main/server/routes/api/v1/__tests__/xpack_info.js delete mode 100644 x-pack/legacy/plugins/xpack_main/server/routes/api/v1/xpack_info.js delete mode 100644 x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts delete mode 100644 x-pack/legacy/server/lib/__tests__/key_case_converter.js delete mode 100644 x-pack/legacy/server/lib/__tests__/kibana_state.js delete mode 100644 x-pack/legacy/server/lib/check_license/check_license.js delete mode 100644 x-pack/legacy/server/lib/check_license/check_license.test.js delete mode 100644 x-pack/legacy/server/lib/check_license/index.js delete mode 100644 x-pack/legacy/server/lib/constants/index.ts delete mode 100644 x-pack/legacy/server/lib/constants/xpack_info.ts delete mode 100644 x-pack/legacy/server/lib/create_router/call_with_request_factory/call_with_request_factory.js delete mode 100644 x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts delete mode 100644 x-pack/legacy/server/lib/create_router/call_with_request_factory/index.js delete mode 100644 x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_custom_error.js delete mode 100644 x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_es_error.js delete mode 100644 x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_unknown_error.js delete mode 100644 x-pack/legacy/server/lib/create_router/error_wrappers/index.d.ts delete mode 100644 x-pack/legacy/server/lib/create_router/error_wrappers/index.js delete mode 100644 x-pack/legacy/server/lib/create_router/error_wrappers/wrap_custom_error.js delete mode 100644 x-pack/legacy/server/lib/create_router/error_wrappers/wrap_es_error.js delete mode 100644 x-pack/legacy/server/lib/create_router/error_wrappers/wrap_unknown_error.js delete mode 100644 x-pack/legacy/server/lib/create_router/index.d.ts delete mode 100644 x-pack/legacy/server/lib/create_router/index.js delete mode 100644 x-pack/legacy/server/lib/create_router/is_es_error_factory/__tests__/is_es_error_factory.js delete mode 100644 x-pack/legacy/server/lib/create_router/is_es_error_factory/index.js delete mode 100644 x-pack/legacy/server/lib/create_router/is_es_error_factory/is_es_error_factory.js delete mode 100644 x-pack/legacy/server/lib/create_router/license_pre_routing_factory/__tests__/license_pre_routing_factory.js delete mode 100644 x-pack/legacy/server/lib/create_router/license_pre_routing_factory/index.js delete mode 100644 x-pack/legacy/server/lib/create_router/license_pre_routing_factory/license_pre_routing_factory.js delete mode 100644 x-pack/legacy/server/lib/key_case_converter.js delete mode 100644 x-pack/legacy/server/lib/parse_kibana_state.js delete mode 100644 x-pack/legacy/server/lib/register_license_checker/index.d.ts delete mode 100644 x-pack/legacy/server/lib/register_license_checker/index.js delete mode 100644 x-pack/legacy/server/lib/register_license_checker/register_license_checker.js delete mode 100644 x-pack/legacy/server/lib/watch_status_and_license_to_initialize.js delete mode 100644 x-pack/legacy/server/lib/watch_status_and_license_to_initialize.test.js delete mode 100644 x-pack/legacy/server/lib/xpack_usage.js rename x-pack/{legacy/common/eui_styled_components => plugins/xpack_legacy/common}/eui_styled_components.tsx (100%) rename x-pack/{legacy/plugins/xpack_main/server/routes/api/v1/index.js => plugins/xpack_legacy/common/index.ts} (83%) diff --git a/.eslintrc.js b/.eslintrc.js index 3161a25b70870..3778bd374da61 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,9 +17,6 @@ * under the License. */ -const { readdirSync } = require('fs'); -const { resolve } = require('path'); - const APACHE_2_0_LICENSE_HEADER = ` /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -288,7 +285,7 @@ module.exports = { }, { target: [ - '(src|x-pack)/legacy/**/*', + 'src/legacy/**/*', '(src|x-pack)/plugins/**/(public|server)/**/*', 'examples/**/*', ], @@ -319,14 +316,11 @@ module.exports = { }, { target: [ - '(src|x-pack)/legacy/**/*', + 'src/legacy/**/*', '(src|x-pack)/plugins/**/(public|server)/**/*', 'examples/**/*', '!(src|x-pack)/**/*.test.*', '!(x-pack/)?test/**/*', - // next folder contains legacy browser tests which can't be migrated to jest - // which import np files - '!src/legacy/core_plugins/kibana/public/__tests__/**/*', ], from: [ '(src|x-pack)/plugins/**/(public|server)/**/*', @@ -341,14 +335,6 @@ module.exports = { '(src|x-pack)/plugins/**/*', '!(src|x-pack)/plugins/**/server/**/*', - 'src/legacy/core_plugins/**/*', - '!src/legacy/core_plugins/**/server/**/*', - '!src/legacy/core_plugins/**/index.{js,mjs,ts,tsx}', - - 'x-pack/legacy/plugins/**/*', - '!x-pack/legacy/plugins/**/server/**/*', - '!x-pack/legacy/plugins/**/index.{js,mjs,ts,tsx}', - 'examples/**/*', '!examples/**/server/**/*', ], @@ -370,12 +356,7 @@ module.exports = { }, { target: ['src/core/**/*'], - from: [ - 'plugins/**/*', - 'src/plugins/**/*', - 'src/legacy/core_plugins/**/*', - 'src/legacy/ui/**/*', - ], + from: ['plugins/**/*', 'src/plugins/**/*', 'src/legacy/ui/**/*'], errorMessage: 'The core cannot depend on any plugins.', }, { @@ -388,12 +369,6 @@ module.exports = { target: [ 'test/plugin_functional/plugins/**/public/np_ready/**/*', 'test/plugin_functional/plugins/**/server/np_ready/**/*', - 'src/legacy/core_plugins/**/public/np_ready/**/*', - 'src/legacy/core_plugins/vis_type_*/public/**/*', - '!src/legacy/core_plugins/vis_type_*/public/legacy*', - 'src/legacy/core_plugins/**/server/np_ready/**/*', - 'x-pack/legacy/plugins/**/public/np_ready/**/*', - 'x-pack/legacy/plugins/**/server/np_ready/**/*', ], allowSameFolder: true, errorMessage: @@ -443,22 +418,14 @@ module.exports = { settings: { // instructs import/no-extraneous-dependencies to treat certain modules // as core modules, even if they aren't listed in package.json - 'import/core-modules': ['plugins', 'legacy/ui'], + 'import/core-modules': ['plugins'], 'import/resolver': { '@kbn/eslint-import-resolver-kibana': { forceNode: false, rootPackageName: 'kibana', kibanaPath: '.', - pluginMap: readdirSync(resolve(__dirname, 'x-pack/legacy/plugins')).reduce( - (acc, name) => { - if (!name.startsWith('_')) { - acc[name] = `x-pack/legacy/plugins/${name}`; - } - return acc; - }, - {} - ), + pluginMap: {}, }, }, }, @@ -764,16 +731,6 @@ module.exports = { }, }, - /** - * GIS overrides - */ - { - files: ['x-pack/legacy/plugins/maps/**/*.js'], - rules: { - 'react/prefer-stateless-function': [0, { ignorePureComponents: false }], - }, - }, - /** * ML overrides */ @@ -812,7 +769,7 @@ module.exports = { }, { // typescript only for front and back end - files: ['x-pack/{,legacy/}plugins/security_solution/**/*.{ts,tsx}'], + files: ['x-pack/plugins/security_solution/**/*.{ts,tsx}'], rules: { // This will be turned on after bug fixes are complete // '@typescript-eslint/explicit-member-accessibility': 'warn', @@ -858,7 +815,7 @@ module.exports = { // }, { // typescript and javascript for front and back end - files: ['x-pack/{,legacy/}plugins/security_solution/**/*.{js,mjs,ts,tsx}'], + files: ['x-pack/plugins/security_solution/**/*.{js,mjs,ts,tsx}'], plugins: ['eslint-plugin-node', 'react'], env: { mocha: true, @@ -1089,7 +1046,7 @@ module.exports = { { // typescript only for front and back end files: [ - 'x-pack/{,legacy/}plugins/{alerts,alerting_builtins,actions,task_manager,event_log}/**/*.{ts,tsx}', + 'x-pack/plugins/{alerts,alerting_builtins,actions,task_manager,event_log}/**/*.{ts,tsx}', ], rules: { '@typescript-eslint/no-explicit-any': 'error', @@ -1238,10 +1195,7 @@ module.exports = { * TSVB overrides */ { - files: [ - 'src/plugins/vis_type_timeseries/**/*.{js,mjs,ts,tsx}', - 'src/legacy/core_plugins/vis_type_timeseries/**/*.{js,mjs,ts,tsx}', - ], + files: ['src/plugins/vis_type_timeseries/**/*.{js,mjs,ts,tsx}'], rules: { 'import/no-default-export': 'error', }, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0bdddddab8de5..0898cfc97f916 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -72,7 +72,7 @@ /x-pack/plugins/apm/server/projections/rum_overview.ts @elastic/uptime # Beats -/x-pack/legacy/plugins/beats_management/ @elastic/beats +/x-pack/plugins/beats_management/ @elastic/beats # Canvas /x-pack/plugins/canvas/ @elastic/kibana-canvas @@ -86,16 +86,13 @@ /x-pack/plugins/global_search_bar/ @elastic/kibana-core-ui # Observability UIs -/x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui /x-pack/plugins/infra/ @elastic/logs-metrics-ui /x-pack/plugins/ingest_manager/ @elastic/ingest-management -/x-pack/legacy/plugins/ingest_manager/ @elastic/ingest-management /x-pack/plugins/observability/ @elastic/observability-ui /x-pack/plugins/monitoring/ @elastic/stack-monitoring-ui /x-pack/plugins/uptime @elastic/uptime # Machine Learning -/x-pack/legacy/plugins/ml/ @elastic/ml-ui /x-pack/plugins/ml/ @elastic/ml-ui /x-pack/test/functional/apps/machine_learning/ @elastic/ml-ui /x-pack/test/functional/services/machine_learning/ @elastic/ml-ui @@ -107,7 +104,6 @@ /x-pack/test/functional/services/transform.ts @elastic/ml-ui # Maps -/x-pack/legacy/plugins/maps/ @elastic/kibana-gis /x-pack/plugins/maps/ @elastic/kibana-gis /x-pack/test/api_integration/apis/maps/ @elastic/kibana-gis /x-pack/test/functional/apps/maps/ @elastic/kibana-gis @@ -234,13 +230,8 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /src/plugins/dev_tools/ @elastic/es-ui /src/plugins/console/ @elastic/es-ui /src/plugins/es_ui_shared/ @elastic/es-ui -/x-pack/legacy/plugins/cross_cluster_replication/ @elastic/es-ui +/x-pack/plugins/cross_cluster_replication/ @elastic/es-ui /x-pack/plugins/index_lifecycle_management/ @elastic/es-ui -/x-pack/legacy/plugins/index_management/ @elastic/es-ui -/x-pack/legacy/plugins/license_management/ @elastic/es-ui -/x-pack/legacy/plugins/rollup/ @elastic/es-ui -/x-pack/legacy/plugins/snapshot_restore/ @elastic/es-ui -/x-pack/legacy/plugins/upgrade_assistant/ @elastic/es-ui /x-pack/plugins/console_extensions/ @elastic/es-ui /x-pack/plugins/es_ui_shared/ @elastic/es-ui /x-pack/plugins/grokdebugger/ @elastic/es-ui diff --git a/kibana.d.ts b/kibana.d.ts index b707405ffbeaf..50f8b8690d944 100644 --- a/kibana.d.ts +++ b/kibana.d.ts @@ -28,7 +28,6 @@ export { Public, Server }; /** * All exports from TS ambient definitions (where types are added for JS source in a .d.ts file). */ -import * as LegacyKibanaPluginSpec from './src/legacy/plugin_discovery/plugin_spec/plugin_spec_options'; import * as LegacyKibanaServer from './src/legacy/server/kbn_server'; /** @@ -39,8 +38,4 @@ export namespace Legacy { export type Request = LegacyKibanaServer.Request; export type ResponseToolkit = LegacyKibanaServer.ResponseToolkit; export type Server = LegacyKibanaServer.Server; - - export type InitPluginFunction = LegacyKibanaPluginSpec.InitPluginFunction; - export type UiExports = LegacyKibanaPluginSpec.UiExports; - export type PluginSpecOptions = LegacyKibanaPluginSpec.PluginSpecOptions; } diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index d8bd39b9dcdf4..a1715cf3dba2c 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -48,11 +48,6 @@ const CAN_CLUSTER = canRequire(CLUSTER_MANAGER_PATH); const REPL_PATH = resolve(__dirname, '../repl'); const CAN_REPL = canRequire(REPL_PATH); -// xpack is installed in both dev and the distributable, it's optional if -// install is a link to the source, not an actual install -const XPACK_DIR = resolve(__dirname, '../../../x-pack'); -const XPACK_INSTALLED = canRequire(XPACK_DIR); - const pathCollector = function () { const paths = []; return function (path) { @@ -137,16 +132,7 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { if (opts.logFile) set('logging.dest', opts.logFile); set('plugins.scanDirs', _.compact([].concat(get('plugins.scanDirs'), opts.pluginDir))); - set( - 'plugins.paths', - _.compact( - [].concat( - get('plugins.paths'), - opts.pluginPath, - XPACK_INSTALLED && !opts.oss ? [XPACK_DIR] : [] - ) - ) - ); + set('plugins.paths', _.compact([].concat(get('plugins.paths'), opts.pluginPath))); merge(extraCliOptions); merge(readKeystore()); diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 5b51bc823d166..bd8c9e91f15a2 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -58,19 +58,6 @@ export interface InjectedMetadataParams { uiPlugins: InjectedPluginMetadata[]; anonymousStatusPage: boolean; legacyMetadata: { - app: { - id: string; - title: string; - }; - bundleId: string; - version: string; - branch: string; - buildNum: number; - buildSha: string; - basePath: string; - serverName: string; - devMode: boolean; - category?: AppCategory; uiSettings: { defaults: Record; user?: Record; @@ -167,18 +154,6 @@ export interface InjectedMetadataSetup { getPlugins: () => InjectedPluginMetadata[]; getAnonymousStatusPage: () => boolean; getLegacyMetadata: () => { - app: { - id: string; - title: string; - }; - bundleId: string; - version: string; - branch: string; - buildNum: number; - buildSha: string; - basePath: string; - serverName: string; - devMode: boolean; uiSettings: { defaults: Record; user?: Record | undefined; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index e136c699f7246..70ef93963c69f 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -339,14 +339,7 @@ export { SavedObjectsMigrationVersion, } from './types'; -export { - LegacyServiceSetupDeps, - LegacyServiceStartDeps, - LegacyServiceDiscoverPlugins, - LegacyConfig, - LegacyUiExports, - LegacyInternals, -} from './legacy'; +export { LegacyServiceSetupDeps, LegacyServiceStartDeps, LegacyConfig } from './legacy'; export { CoreStatus, diff --git a/src/core/server/legacy/config/ensure_valid_configuration.test.ts b/src/core/server/legacy/config/ensure_valid_configuration.test.ts index 702840b8a0a6a..700fe69954655 100644 --- a/src/core/server/legacy/config/ensure_valid_configuration.test.ts +++ b/src/core/server/legacy/config/ensure_valid_configuration.test.ts @@ -39,17 +39,12 @@ describe('ensureValidConfiguration', () => { configService as any, { settings: 'settings', - pluginSpecs: 'pluginSpecs', - disabledPluginSpecs: 'disabledPluginSpecs', - pluginExtendedConfig: 'pluginExtendedConfig', - uiExports: 'uiExports', + legacyConfig: 'pluginExtendedConfig', } as any ); expect(getUnusedConfigKeys).toHaveBeenCalledTimes(1); expect(getUnusedConfigKeys).toHaveBeenCalledWith({ coreHandledConfigPaths: ['core', 'elastic'], - pluginSpecs: 'pluginSpecs', - disabledPluginSpecs: 'disabledPluginSpecs', settings: 'settings', legacyConfig: 'pluginExtendedConfig', }); diff --git a/src/core/server/legacy/config/ensure_valid_configuration.ts b/src/core/server/legacy/config/ensure_valid_configuration.ts index 5cd1603ea65fb..34f98b9b3a795 100644 --- a/src/core/server/legacy/config/ensure_valid_configuration.ts +++ b/src/core/server/legacy/config/ensure_valid_configuration.ts @@ -19,19 +19,17 @@ import { getUnusedConfigKeys } from './get_unused_config_keys'; import { ConfigService } from '../../config'; -import { LegacyServiceDiscoverPlugins } from '../types'; import { CriticalError } from '../../errors'; +import { LegacyServiceSetupConfig } from '../types'; export async function ensureValidConfiguration( configService: ConfigService, - { pluginSpecs, disabledPluginSpecs, pluginExtendedConfig, settings }: LegacyServiceDiscoverPlugins + { legacyConfig, settings }: LegacyServiceSetupConfig ) { const unusedConfigKeys = await getUnusedConfigKeys({ coreHandledConfigPaths: await configService.getUsedPaths(), - pluginSpecs, - disabledPluginSpecs, settings, - legacyConfig: pluginExtendedConfig, + legacyConfig, }); if (unusedConfigKeys.length > 0) { diff --git a/src/core/server/legacy/config/get_unused_config_keys.test.ts b/src/core/server/legacy/config/get_unused_config_keys.test.ts index f8506b5744030..6ce69fca0270a 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.test.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { LegacyPluginSpec, LegacyConfig, LegacyVars } from '../types'; +import { LegacyConfig, LegacyVars } from '../types'; import { getUnusedConfigKeys } from './get_unused_config_keys'; describe('getUnusedConfigKeys', () => { @@ -35,8 +35,6 @@ describe('getUnusedConfigKeys', () => { expect( await getUnusedConfigKeys({ coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [], settings: {}, legacyConfig: getConfig(), }) @@ -47,8 +45,6 @@ describe('getUnusedConfigKeys', () => { expect( await getUnusedConfigKeys({ coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [], settings: { presentInBoth: true, alsoInBoth: 'someValue', @@ -65,8 +61,6 @@ describe('getUnusedConfigKeys', () => { expect( await getUnusedConfigKeys({ coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [], settings: { presentInBoth: true, }, @@ -82,8 +76,6 @@ describe('getUnusedConfigKeys', () => { expect( await getUnusedConfigKeys({ coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [], settings: { presentInBoth: true, onlyInSetting: 'value', @@ -99,8 +91,6 @@ describe('getUnusedConfigKeys', () => { expect( await getUnusedConfigKeys({ coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [], settings: { elasticsearch: { username: 'foo', @@ -121,8 +111,6 @@ describe('getUnusedConfigKeys', () => { expect( await getUnusedConfigKeys({ coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [], settings: { env: 'development', }, @@ -139,8 +127,6 @@ describe('getUnusedConfigKeys', () => { expect( await getUnusedConfigKeys({ coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [], settings: { prop: ['a', 'b', 'c'], }, @@ -152,40 +138,10 @@ describe('getUnusedConfigKeys', () => { }); }); - it('ignores config for plugins that are disabled', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [ - ({ - id: 'foo', - getConfigPrefix: () => 'foo.bar', - } as unknown) as LegacyPluginSpec, - ], - settings: { - foo: { - bar: { - unused: true, - }, - }, - plugin: { - missingProp: false, - }, - }, - legacyConfig: getConfig({ - prop: 'a', - }), - }) - ).toEqual(['plugin.missingProp']); - }); - it('ignores properties managed by the new platform', async () => { expect( await getUnusedConfigKeys({ coreHandledConfigPaths: ['core', 'foo.bar'], - pluginSpecs: [], - disabledPluginSpecs: [], settings: { core: { prop: 'value', @@ -204,8 +160,6 @@ describe('getUnusedConfigKeys', () => { expect( await getUnusedConfigKeys({ coreHandledConfigPaths: ['core', 'array'], - pluginSpecs: [], - disabledPluginSpecs: [], settings: { core: { prop: 'value', diff --git a/src/core/server/legacy/config/get_unused_config_keys.ts b/src/core/server/legacy/config/get_unused_config_keys.ts index c15c3b270df05..5bbe169033e39 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.ts @@ -19,30 +19,20 @@ import { difference } from 'lodash'; import { getFlattenedObject } from '@kbn/std'; -import { unset } from '../../../../legacy/utils'; import { hasConfigPathIntersection } from '../../config'; -import { LegacyPluginSpec, LegacyConfig, LegacyVars } from '../types'; +import { LegacyConfig, LegacyVars } from '../types'; const getFlattenedKeys = (object: object) => Object.keys(getFlattenedObject(object)); export async function getUnusedConfigKeys({ coreHandledConfigPaths, - pluginSpecs, - disabledPluginSpecs, settings, legacyConfig, }: { coreHandledConfigPaths: string[]; - pluginSpecs: LegacyPluginSpec[]; - disabledPluginSpecs: LegacyPluginSpec[]; settings: LegacyVars; legacyConfig: LegacyConfig; }) { - // remove config values from disabled plugins - for (const spec of disabledPluginSpecs) { - unset(settings, spec.getConfigPrefix()); - } - const inputKeys = getFlattenedKeys(settings); const appliedKeys = getFlattenedKeys(legacyConfig.get()); diff --git a/src/core/server/legacy/index.ts b/src/core/server/legacy/index.ts index 6b0963e3129c6..1a0bc8955be0f 100644 --- a/src/core/server/legacy/index.ts +++ b/src/core/server/legacy/index.ts @@ -20,8 +20,6 @@ /** @internal */ export { ensureValidConfiguration } from './config'; /** @internal */ -export { LegacyInternals } from './legacy_internals'; -/** @internal */ export { LegacyService, ILegacyService } from './legacy_service'; /** @internal */ export * from './types'; diff --git a/src/core/server/legacy/legacy_internals.test.ts b/src/core/server/legacy/legacy_internals.test.ts deleted file mode 100644 index 935e36a989a0c..0000000000000 --- a/src/core/server/legacy/legacy_internals.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Server } from 'hapi'; - -import { configMock } from '../config/mocks'; -import { httpServiceMock } from '../http/http_service.mock'; -import { httpServerMock } from '../http/http_server.mocks'; -import { findLegacyPluginSpecsMock } from './legacy_service.test.mocks'; -import { LegacyInternals } from './legacy_internals'; -import { ILegacyInternals, LegacyConfig, LegacyVars, LegacyUiExports } from './types'; - -function varsProvider(vars: LegacyVars, configValue?: any) { - return { - fn: jest.fn().mockReturnValue(vars), - pluginSpec: { - readConfigValue: jest.fn().mockReturnValue(configValue), - }, - }; -} - -describe('LegacyInternals', () => { - describe('getInjectedUiAppVars()', () => { - let uiExports: LegacyUiExports; - let config: LegacyConfig; - let server: Server; - let legacyInternals: ILegacyInternals; - - beforeEach(async () => { - uiExports = findLegacyPluginSpecsMock().uiExports; - config = configMock.create() as any; - server = httpServiceMock.createInternalSetupContract().server; - legacyInternals = new LegacyInternals(uiExports, config, server); - }); - - it('gets with no injectors', async () => { - await expect(legacyInternals.getInjectedUiAppVars('core')).resolves.toMatchInlineSnapshot( - `Object {}` - ); - }); - - it('gets with no matching injectors', async () => { - const injector = jest.fn().mockResolvedValue({ not: 'core' }); - legacyInternals.injectUiAppVars('not-core', injector); - - await expect(legacyInternals.getInjectedUiAppVars('core')).resolves.toMatchInlineSnapshot( - `Object {}` - ); - expect(injector).not.toHaveBeenCalled(); - }); - - it('gets with single matching injector', async () => { - const injector = jest.fn().mockResolvedValue({ is: 'core' }); - legacyInternals.injectUiAppVars('core', injector); - - await expect(legacyInternals.getInjectedUiAppVars('core')).resolves.toMatchInlineSnapshot(` - Object { - "is": "core", - } - `); - expect(injector).toHaveBeenCalled(); - }); - - it('gets with multiple matching injectors', async () => { - const injectors = [ - jest.fn().mockResolvedValue({ is: 'core' }), - jest.fn().mockReturnValue({ sync: 'injector' }), - jest.fn().mockResolvedValue({ is: 'merged-core' }), - ]; - - injectors.forEach((injector) => legacyInternals.injectUiAppVars('core', injector)); - - await expect(legacyInternals.getInjectedUiAppVars('core')).resolves.toMatchInlineSnapshot(` - Object { - "is": "merged-core", - "sync": "injector", - } - `); - expect(injectors[0]).toHaveBeenCalled(); - expect(injectors[1]).toHaveBeenCalled(); - expect(injectors[2]).toHaveBeenCalled(); - }); - }); - - describe('getVars()', () => { - let uiExports: LegacyUiExports; - let config: LegacyConfig; - let server: Server; - let legacyInternals: LegacyInternals; - - beforeEach(async () => { - uiExports = findLegacyPluginSpecsMock().uiExports; - config = configMock.create() as any; - server = httpServiceMock.createInternalSetupContract().server; - legacyInternals = new LegacyInternals(uiExports, config, server); - }); - - it('gets: no default injectors, no injected vars replacers, no ui app injectors, no inject arg', async () => { - const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest()); - - expect(vars).toMatchInlineSnapshot(`Object {}`); - }); - - it('gets: with default injectors, no injected vars replacers, no ui app injectors, no inject arg', async () => { - uiExports.defaultInjectedVarProviders = [ - varsProvider({ alpha: 'alpha' }), - varsProvider({ gamma: 'gamma' }), - varsProvider({ alpha: 'beta' }), - ]; - - const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest()); - - expect(vars).toMatchInlineSnapshot(` - Object { - "alpha": "beta", - "gamma": "gamma", - } - `); - }); - - it('gets: no default injectors, with injected vars replacers, with ui app injectors, no inject arg', async () => { - uiExports.injectedVarsReplacers = [ - jest.fn(async (vars) => ({ ...vars, added: 'key' })), - jest.fn((vars) => vars), - jest.fn((vars) => ({ replaced: 'all' })), - jest.fn(async (vars) => ({ ...vars, added: 'last-key' })), - ]; - - const request = httpServerMock.createRawRequest(); - const vars = await legacyInternals.getVars('core', request); - - expect(vars).toMatchInlineSnapshot(` - Object { - "added": "last-key", - "replaced": "all", - } - `); - }); - - it('gets: no default injectors, no injected vars replacers, with ui app injectors, no inject arg', async () => { - legacyInternals.injectUiAppVars('core', async () => ({ is: 'core' })); - legacyInternals.injectUiAppVars('core', () => ({ sync: 'injector' })); - legacyInternals.injectUiAppVars('core', async () => ({ is: 'merged-core' })); - - const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest()); - - expect(vars).toMatchInlineSnapshot(` - Object { - "is": "merged-core", - "sync": "injector", - } - `); - }); - - it('gets: no default injectors, no injected vars replacers, no ui app injectors, with inject arg', async () => { - const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest(), { - injected: 'arg', - }); - - expect(vars).toMatchInlineSnapshot(` - Object { - "injected": "arg", - } - `); - }); - - it('gets: with default injectors, with injected vars replacers, with ui app injectors, with inject arg', async () => { - uiExports.defaultInjectedVarProviders = [ - varsProvider({ alpha: 'alpha' }), - varsProvider({ gamma: 'gamma' }), - varsProvider({ alpha: 'beta' }), - ]; - uiExports.injectedVarsReplacers = [jest.fn(async (vars) => ({ ...vars, gamma: 'delta' }))]; - - legacyInternals.injectUiAppVars('core', async () => ({ is: 'core' })); - legacyInternals.injectUiAppVars('core', () => ({ sync: 'injector' })); - legacyInternals.injectUiAppVars('core', async () => ({ is: 'merged-core' })); - - const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest(), { - injected: 'arg', - sync: 'arg', - }); - - expect(vars).toMatchInlineSnapshot(` - Object { - "alpha": "beta", - "gamma": "delta", - "injected": "arg", - "is": "merged-core", - "sync": "arg", - } - `); - }); - }); -}); diff --git a/src/core/server/legacy/legacy_internals.ts b/src/core/server/legacy/legacy_internals.ts deleted file mode 100644 index 628ca4ed12f6b..0000000000000 --- a/src/core/server/legacy/legacy_internals.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Server } from 'hapi'; - -import { KibanaRequest, LegacyRequest } from '../http'; -import { ensureRawRequest } from '../http/router'; -import { mergeVars } from './merge_vars'; -import { ILegacyInternals, LegacyVars, VarsInjector, LegacyConfig, LegacyUiExports } from './types'; - -/** - * @internal - * @deprecated - */ -export class LegacyInternals implements ILegacyInternals { - private readonly injectors = new Map>(); - private cachedDefaultVars?: LegacyVars; - - constructor( - private readonly uiExports: LegacyUiExports, - private readonly config: LegacyConfig, - private readonly server: Server - ) {} - - private get defaultVars(): LegacyVars { - if (this.cachedDefaultVars) { - return this.cachedDefaultVars; - } - - const { defaultInjectedVarProviders = [] } = this.uiExports; - - return (this.cachedDefaultVars = defaultInjectedVarProviders.reduce( - (vars, { fn, pluginSpec }) => - mergeVars(vars, fn(this.server, pluginSpec.readConfigValue(this.config, []))), - {} - )); - } - - private replaceVars(vars: LegacyVars, request: KibanaRequest | LegacyRequest) { - const { injectedVarsReplacers = [] } = this.uiExports; - - return injectedVarsReplacers.reduce( - async (injected, replacer) => - replacer(await injected, ensureRawRequest(request), this.server), - Promise.resolve(vars) - ); - } - - public injectUiAppVars(id: string, injector: VarsInjector) { - if (!this.injectors.has(id)) { - this.injectors.set(id, new Set()); - } - - this.injectors.get(id)!.add(injector); - } - - public getInjectedUiAppVars(id: string) { - return [...(this.injectors.get(id) || [])].reduce( - async (promise, injector) => ({ - ...(await promise), - ...(await injector()), - }), - Promise.resolve({}) - ); - } - - public async getVars( - id: string, - request: KibanaRequest | LegacyRequest, - injected: LegacyVars = {} - ) { - return this.replaceVars( - mergeVars(this.defaultVars, await this.getInjectedUiAppVars(id), injected), - request - ); - } -} diff --git a/src/core/server/legacy/legacy_service.mock.ts b/src/core/server/legacy/legacy_service.mock.ts index ab501bd6bb53b..781874f702cf8 100644 --- a/src/core/server/legacy/legacy_service.mock.ts +++ b/src/core/server/legacy/legacy_service.mock.ts @@ -18,26 +18,13 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; import { LegacyService } from './legacy_service'; -import { LegacyConfig, LegacyServiceDiscoverPlugins, LegacyServiceSetupDeps } from './types'; +import { LegacyConfig, LegacyServiceSetupDeps } from './types'; type LegacyServiceMock = jest.Mocked & { legacyId: symbol }>; -const createDiscoverPluginsMock = (): LegacyServiceDiscoverPlugins => ({ - pluginSpecs: [], - uiExports: {}, - navLinks: [], - pluginExtendedConfig: { - get: jest.fn(), - has: jest.fn(), - set: jest.fn(), - }, - disabledPluginSpecs: [], - settings: {}, -}); - const createLegacyServiceMock = (): LegacyServiceMock => ({ legacyId: Symbol(), - discoverPlugins: jest.fn().mockResolvedValue(createDiscoverPluginsMock()), + setupLegacyConfig: jest.fn(), setup: jest.fn(), start: jest.fn(), stop: jest.fn(), @@ -52,6 +39,5 @@ const createLegacyConfigMock = (): jest.Mocked => ({ export const legacyServiceMock = { create: createLegacyServiceMock, createSetupContract: (deps: LegacyServiceSetupDeps) => createLegacyServiceMock().setup(deps), - createDiscoverPlugins: createDiscoverPluginsMock, createLegacyConfig: createLegacyConfigMock, }; diff --git a/src/core/server/legacy/legacy_service.test.mocks.ts b/src/core/server/legacy/legacy_service.test.mocks.ts deleted file mode 100644 index 9ad554d63add0..0000000000000 --- a/src/core/server/legacy/legacy_service.test.mocks.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { LegacyVars } from './types'; - -export const findLegacyPluginSpecsMock = jest.fn().mockImplementation((settings: LegacyVars) => ({ - pluginSpecs: [], - pluginExtendedConfig: { - has: jest.fn(), - get: jest.fn().mockReturnValue(settings), - set: jest.fn(), - }, - disabledPluginSpecs: [], - uiExports: {}, - navLinks: [], -})); -jest.doMock('./plugins/find_legacy_plugin_specs', () => ({ - findLegacyPluginSpecs: findLegacyPluginSpecsMock, -})); - -export const logLegacyThirdPartyPluginDeprecationWarningMock = jest.fn(); -jest.doMock('./plugins/log_legacy_plugins_warning', () => ({ - logLegacyThirdPartyPluginDeprecationWarning: logLegacyThirdPartyPluginDeprecationWarningMock, -})); diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index a6fe95deb3979..57009f0d35c16 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -19,10 +19,6 @@ jest.mock('../../../legacy/server/kbn_server'); jest.mock('./cluster_manager'); -import { - findLegacyPluginSpecsMock, - logLegacyThirdPartyPluginDeprecationWarningMock, -} from './legacy_service.test.mocks'; import { BehaviorSubject, throwError } from 'rxjs'; import { REPO_ROOT } from '@kbn/dev-utils'; @@ -44,8 +40,7 @@ import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mo import { httpResourcesMock } from '../http_resources/http_resources_service.mock'; import { setupMock as renderingServiceMock } from '../rendering/__mocks__/rendering_service'; import { environmentServiceMock } from '../environment/environment_service.mock'; -import { findLegacyPluginSpecs } from './plugins'; -import { LegacyVars, LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types'; +import { LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types'; import { LegacyService } from './legacy_service'; import { coreMock } from '../mocks'; import { statusServiceMock } from '../status/status_service.mock'; @@ -73,7 +68,6 @@ beforeEach(() => { configService = configServiceMock.create(); environmentSetup = environmentServiceMock.createSetupContract(); - findLegacyPluginSpecsMock.mockClear(); MockKbnServer.prototype.ready = jest.fn().mockReturnValue(Promise.resolve()); MockKbnServer.prototype.listen = jest.fn(); @@ -149,10 +143,10 @@ describe('once LegacyService is set up with connection info', () => { coreId, env, logger, - configService: configService as any, + configService, }); - await legacyService.discoverPlugins(); + await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -160,13 +154,14 @@ describe('once LegacyService is set up with connection info', () => { expect(MockKbnServer).toHaveBeenCalledWith( { path: { autoListen: true }, server: { autoListen: true } }, // Because of the mock, path also gets the value expect.objectContaining({ get: expect.any(Function) }), - expect.any(Object), - { disabledPluginSpecs: [], pluginSpecs: [], uiExports: {}, navLinks: [] } + expect.any(Object) + ); + expect(MockKbnServer.mock.calls[0][1].get()).toEqual( + expect.objectContaining({ + path: expect.objectContaining({ autoListen: true }), + server: expect.objectContaining({ autoListen: true }), + }) ); - expect(MockKbnServer.mock.calls[0][1].get()).toEqual({ - path: { autoListen: true }, - server: { autoListen: true }, - }); const [mockKbnServer] = MockKbnServer.mock.instances; expect(mockKbnServer.listen).toHaveBeenCalledTimes(1); @@ -182,7 +177,7 @@ describe('once LegacyService is set up with connection info', () => { logger, configService: configService as any, }); - await legacyService.discoverPlugins(); + await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -190,13 +185,12 @@ describe('once LegacyService is set up with connection info', () => { expect(MockKbnServer).toHaveBeenCalledWith( { path: { autoListen: false }, server: { autoListen: true } }, expect.objectContaining({ get: expect.any(Function) }), - expect.any(Object), - { disabledPluginSpecs: [], pluginSpecs: [], uiExports: {}, navLinks: [] } + expect.any(Object) ); - expect(MockKbnServer.mock.calls[0][1].get()).toEqual({ - path: { autoListen: false }, - server: { autoListen: true }, - }); + + const legacyConfig = MockKbnServer.mock.calls[0][1].get(); + expect(legacyConfig.path.autoListen).toBe(false); + expect(legacyConfig.server.autoListen).toBe(true); const [mockKbnServer] = MockKbnServer.mock.instances; expect(mockKbnServer.ready).toHaveBeenCalledTimes(1); @@ -214,7 +208,7 @@ describe('once LegacyService is set up with connection info', () => { configService: configService as any, }); - await legacyService.discoverPlugins(); + await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( `"something failed"` @@ -234,11 +228,11 @@ describe('once LegacyService is set up with connection info', () => { configService: configService as any, }); - await expect(legacyService.discoverPlugins()).rejects.toThrowErrorMatchingInlineSnapshot( + await expect(legacyService.setupLegacyConfig()).rejects.toThrowErrorMatchingInlineSnapshot( `"something failed"` ); await expect(legacyService.setup(setupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Legacy service has not discovered legacy plugins yet. Ensure LegacyService.discoverPlugins() is called before LegacyService.setup()"` + `"Legacy config not initialized yet. Ensure LegacyService.setupLegacyConfig() is called before LegacyService.setup()"` ); await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( `"Legacy service is not setup yet."` @@ -255,7 +249,7 @@ describe('once LegacyService is set up with connection info', () => { logger, configService: configService as any, }); - await legacyService.discoverPlugins(); + await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -276,7 +270,7 @@ describe('once LegacyService is set up with connection info', () => { logger, configService: configService as any, }); - await legacyService.discoverPlugins(); + await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -301,7 +295,7 @@ describe('once LegacyService is set up with connection info', () => { logger, configService: configService as any, }); - await legacyService.discoverPlugins(); + await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -321,7 +315,7 @@ describe('once LegacyService is set up without connection info', () => { let legacyService: LegacyService; beforeEach(async () => { legacyService = new LegacyService({ coreId, env, logger, configService: configService as any }); - await legacyService.discoverPlugins(); + await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); await legacyService.start(startDeps); }); @@ -331,13 +325,13 @@ describe('once LegacyService is set up without connection info', () => { expect(MockKbnServer).toHaveBeenCalledWith( { path: {}, server: { autoListen: true } }, expect.objectContaining({ get: expect.any(Function) }), - expect.any(Object), - { disabledPluginSpecs: [], pluginSpecs: [], uiExports: {}, navLinks: [] } + expect.any(Object) + ); + expect(MockKbnServer.mock.calls[0][1].get()).toEqual( + expect.objectContaining({ + server: expect.objectContaining({ autoListen: true }), + }) ); - expect(MockKbnServer.mock.calls[0][1].get()).toEqual({ - path: {}, - server: { autoListen: true }, - }); }); test('reconfigures logging configuration if new config is received.', async () => { @@ -375,7 +369,7 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { configService: configService as any, }); - await devClusterLegacyService.discoverPlugins(); + await devClusterLegacyService.setupLegacyConfig(); await devClusterLegacyService.setup(setupDeps); await devClusterLegacyService.start(startDeps); @@ -404,7 +398,7 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { configService: configService as any, }); - await devClusterLegacyService.discoverPlugins(); + await devClusterLegacyService.setupLegacyConfig(); await devClusterLegacyService.setup(setupDeps); await devClusterLegacyService.start(startDeps); @@ -434,50 +428,6 @@ describe('start', () => { }); }); -describe('#discoverPlugins()', () => { - it('calls findLegacyPluginSpecs with correct parameters', async () => { - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, - }); - - await legacyService.discoverPlugins(); - expect(findLegacyPluginSpecs).toHaveBeenCalledTimes(1); - expect(findLegacyPluginSpecs).toHaveBeenCalledWith(expect.any(Object), logger, env.packageInfo); - }); - - it(`logs deprecations for legacy third party plugins`, async () => { - const pluginSpecs = [{ getId: () => 'pluginA' }, { getId: () => 'pluginB' }]; - findLegacyPluginSpecsMock.mockImplementation( - (settings) => - Promise.resolve({ - pluginSpecs, - pluginExtendedConfig: settings, - disabledPluginSpecs: [], - uiExports: {}, - navLinks: [], - }) as any - ); - - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, - }); - - await legacyService.discoverPlugins(); - - expect(logLegacyThirdPartyPluginDeprecationWarningMock).toHaveBeenCalledTimes(1); - expect(logLegacyThirdPartyPluginDeprecationWarningMock).toHaveBeenCalledWith({ - specs: pluginSpecs, - log: expect.any(Object), - }); - }); -}); - test('Sets the server.uuid property on the legacy configuration', async () => { configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); const legacyService = new LegacyService({ @@ -489,23 +439,8 @@ test('Sets the server.uuid property on the legacy configuration', async () => { environmentSetup.instanceUuid = 'UUID_FROM_SERVICE'; - const configSetMock = jest.fn(); - - findLegacyPluginSpecsMock.mockImplementation((settings: LegacyVars) => ({ - pluginSpecs: [], - pluginExtendedConfig: { - has: jest.fn(), - get: jest.fn().mockReturnValue(settings), - set: configSetMock, - }, - disabledPluginSpecs: [], - uiExports: {}, - navLinks: [], - })); - - await legacyService.discoverPlugins(); + const { legacyConfig } = await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); - expect(configSetMock).toHaveBeenCalledTimes(1); - expect(configSetMock).toHaveBeenCalledWith('server.uuid', 'UUID_FROM_SERVICE'); + expect(legacyConfig.get('server.uuid')).toBe('UUID_FROM_SERVICE'); }); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 4dc22be2a9971..086e20c98c1a3 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -16,11 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import type { PublicMethodsOf } from '@kbn/utility-types'; + import { combineLatest, ConnectableObservable, EMPTY, Observable, Subscription } from 'rxjs'; import { first, map, publishReplay, tap } from 'rxjs/operators'; - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { PathConfigType } from '@kbn/utils'; + +// @ts-expect-error legacy config class +import { Config as LegacyConfigClass } from '../../../legacy/server/config'; import { CoreService } from '../../types'; import { Config } from '../config'; import { CoreContext } from '../core_context'; @@ -28,17 +31,7 @@ import { CspConfigType, config as cspConfig } from '../csp'; import { DevConfig, DevConfigType, config as devConfig } from '../dev'; import { BasePathProxyServer, HttpConfig, HttpConfigType, config as httpConfig } from '../http'; import { Logger } from '../logging'; -import { findLegacyPluginSpecs, logLegacyThirdPartyPluginDeprecationWarning } from './plugins'; -import { - ILegacyInternals, - LegacyServiceSetupDeps, - LegacyServiceStartDeps, - LegacyPlugins, - LegacyServiceDiscoverPlugins, - LegacyConfig, - LegacyVars, -} from './types'; -import { LegacyInternals } from './legacy_internals'; +import { LegacyServiceSetupDeps, LegacyServiceStartDeps, LegacyConfig, LegacyVars } from './types'; import { CoreSetup, CoreStart } from '..'; interface LegacyKbnServer { @@ -80,9 +73,7 @@ export class LegacyService implements CoreService { private setupDeps?: LegacyServiceSetupDeps; private update$?: ConnectableObservable<[Config, PathConfigType]>; private legacyRawConfig?: LegacyConfig; - private legacyPlugins?: LegacyPlugins; private settings?: LegacyVars; - public legacyInternals?: ILegacyInternals; constructor(private readonly coreContext: CoreContext) { const { logger, configService } = coreContext; @@ -97,11 +88,11 @@ export class LegacyService implements CoreService { ).pipe(map(([http, csp]) => new HttpConfig(http, csp))); } - public async discoverPlugins(): Promise { - this.update$ = combineLatest( + public async setupLegacyConfig() { + this.update$ = combineLatest([ this.coreContext.configService.getConfig$(), - this.coreContext.configService.atPath('path') - ).pipe( + this.coreContext.configService.atPath('path'), + ]).pipe( tap(([config, pathConfig]) => { if (this.kbnServer !== undefined) { this.kbnServer.applyLoggingConfiguration(getLegacyRawConfig(config, pathConfig)); @@ -120,74 +111,33 @@ export class LegacyService implements CoreService { ) .toPromise(); - const { - pluginSpecs, - pluginExtendedConfig, - disabledPluginSpecs, - uiExports, - navLinks, - } = await findLegacyPluginSpecs( - this.settings, - this.coreContext.logger, - this.coreContext.env.packageInfo - ); - - logLegacyThirdPartyPluginDeprecationWarning({ - specs: pluginSpecs, - log: this.log, - }); - - this.legacyPlugins = { - pluginSpecs, - disabledPluginSpecs, - uiExports, - navLinks, - }; - - this.legacyRawConfig = pluginExtendedConfig; - - // check for unknown uiExport types - if (uiExports.unknown && uiExports.unknown.length > 0) { - throw new Error( - `Unknown uiExport types: ${uiExports.unknown - .map(({ pluginSpec, type }) => `${type} from ${pluginSpec.getId()}`) - .join(', ')}` - ); - } + this.legacyRawConfig = LegacyConfigClass.withDefaultSchema(this.settings); return { - pluginSpecs, - disabledPluginSpecs, - uiExports, - navLinks, - pluginExtendedConfig, settings: this.settings, + legacyConfig: this.legacyRawConfig!, }; } public async setup(setupDeps: LegacyServiceSetupDeps) { this.log.debug('setting up legacy service'); - if (!this.legacyPlugins) { + if (!this.legacyRawConfig) { throw new Error( - 'Legacy service has not discovered legacy plugins yet. Ensure LegacyService.discoverPlugins() is called before LegacyService.setup()' + 'Legacy config not initialized yet. Ensure LegacyService.setupLegacyConfig() is called before LegacyService.setup()' ); } // propagate the instance uuid to the legacy config, as it was the legacy way to access it. this.legacyRawConfig!.set('server.uuid', setupDeps.core.environment.instanceUuid); + this.setupDeps = setupDeps; - this.legacyInternals = new LegacyInternals( - this.legacyPlugins.uiExports, - this.legacyRawConfig!, - setupDeps.core.http.server - ); } public async start(startDeps: LegacyServiceStartDeps) { const { setupDeps } = this; - if (!setupDeps || !this.legacyPlugins) { + if (!setupDeps || !this.legacyRawConfig) { throw new Error('Legacy service is not setup yet.'); } @@ -201,8 +151,7 @@ export class LegacyService implements CoreService { this.settings!, this.legacyRawConfig!, setupDeps, - startDeps, - this.legacyPlugins! + startDeps ); } } @@ -245,8 +194,7 @@ export class LegacyService implements CoreService { settings: LegacyVars, config: LegacyConfig, setupDeps: LegacyServiceSetupDeps, - startDeps: LegacyServiceStartDeps, - legacyPlugins: LegacyPlugins + startDeps: LegacyServiceStartDeps ) { const coreStart: CoreStart = { capabilities: startDeps.core.capabilities, @@ -337,36 +285,26 @@ export class LegacyService implements CoreService { // eslint-disable-next-line @typescript-eslint/no-var-requires const KbnServer = require('../../../legacy/server/kbn_server'); - const kbnServer: LegacyKbnServer = new KbnServer( - settings, - config, - { - env: { - mode: this.coreContext.env.mode, - packageInfo: this.coreContext.env.packageInfo, - }, - setupDeps: { - core: coreSetup, - plugins: setupDeps.plugins, - }, - startDeps: { - core: coreStart, - plugins: startDeps.plugins, - }, - __internals: { - http: { - registerStaticDir: setupDeps.core.http.registerStaticDir, - }, - hapiServer: setupDeps.core.http.server, - uiPlugins: setupDeps.uiPlugins, - elasticsearch: setupDeps.core.elasticsearch, - rendering: setupDeps.core.rendering, - legacy: this.legacyInternals, - }, - logger: this.coreContext.logger, + const kbnServer: LegacyKbnServer = new KbnServer(settings, config, { + env: { + mode: this.coreContext.env.mode, + packageInfo: this.coreContext.env.packageInfo, }, - legacyPlugins - ); + setupDeps: { + core: coreSetup, + plugins: setupDeps.plugins, + }, + startDeps: { + core: coreStart, + plugins: startDeps.plugins, + }, + __internals: { + hapiServer: setupDeps.core.http.server, + uiPlugins: setupDeps.uiPlugins, + rendering: setupDeps.core.rendering, + }, + logger: this.coreContext.logger, + }); // The kbnWorkerType check is necessary to prevent the repl // from being started multiple times in different processes. diff --git a/src/core/server/legacy/plugins/collect_ui_exports.js b/src/core/server/legacy/plugins/collect_ui_exports.js deleted file mode 100644 index 842ab554d79d1..0000000000000 --- a/src/core/server/legacy/plugins/collect_ui_exports.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { collectUiExports } from '../../../../legacy/ui/ui_exports/collect_ui_exports'; diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts deleted file mode 100644 index cb4277b130a88..0000000000000 --- a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Observable, merge, forkJoin } from 'rxjs'; -import { toArray, tap, distinct, map } from 'rxjs/operators'; - -import { - findPluginSpecs, - defaultConfig, - // @ts-expect-error -} from '../../../../legacy/plugin_discovery/find_plugin_specs.js'; -// @ts-expect-error -import { collectUiExports as collectLegacyUiExports } from './collect_ui_exports'; - -import { LoggerFactory } from '../../logging'; -import { PackageInfo } from '../../config'; -import { LegacyUiExports, LegacyPluginSpec, LegacyPluginPack, LegacyConfig } from '../types'; - -export async function findLegacyPluginSpecs( - settings: unknown, - loggerFactory: LoggerFactory, - packageInfo: PackageInfo -) { - const configToMutate: LegacyConfig = defaultConfig(settings); - const { - pack$, - invalidDirectoryError$, - invalidPackError$, - otherError$, - deprecation$, - invalidVersionSpec$, - spec$, - disabledSpec$, - }: { - pack$: Observable; - invalidDirectoryError$: Observable<{ path: string }>; - invalidPackError$: Observable<{ path: string }>; - otherError$: Observable; - deprecation$: Observable<{ spec: LegacyPluginSpec; message: string }>; - invalidVersionSpec$: Observable; - spec$: Observable; - disabledSpec$: Observable; - } = findPluginSpecs(settings, configToMutate) as any; - - const logger = loggerFactory.get('legacy-plugins'); - - const log$ = merge( - pack$.pipe( - tap((definition) => { - const path = definition.getPath(); - logger.debug(`Found plugin at ${path}`, { path }); - }) - ), - - invalidDirectoryError$.pipe( - tap((error) => { - logger.warn(`Unable to scan directory for plugins "${error.path}"`, { - err: error, - dir: error.path, - }); - }) - ), - - invalidPackError$.pipe( - tap((error) => { - logger.warn(`Skipping non-plugin directory at ${error.path}`, { - path: error.path, - }); - }) - ), - - otherError$.pipe( - tap((error) => { - // rethrow unhandled errors, which will fail the server - throw error; - }) - ), - - invalidVersionSpec$.pipe( - map((spec) => { - const name = spec.getId(); - const pluginVersion = spec.getExpectedKibanaVersion(); - const kibanaVersion = packageInfo.version; - return `Plugin "${name}" was disabled because it expected Kibana version "${pluginVersion}", and found "${kibanaVersion}".`; - }), - distinct(), - tap((message) => { - logger.warn(message); - }) - ), - - deprecation$.pipe( - tap(({ spec, message }) => { - const deprecationLogger = loggerFactory.get( - 'plugins', - spec.getConfigPrefix(), - 'config', - 'deprecation' - ); - deprecationLogger.warn(message); - }) - ) - ); - - const [disabledPluginSpecs, pluginSpecs] = await forkJoin( - disabledSpec$.pipe(toArray()), - spec$.pipe(toArray()), - log$.pipe(toArray()) - ).toPromise(); - const uiExports: LegacyUiExports = collectLegacyUiExports(pluginSpecs); - - return { - disabledPluginSpecs, - pluginSpecs, - pluginExtendedConfig: configToMutate, - uiExports, - navLinks: [], - }; -} diff --git a/src/core/server/legacy/plugins/index.ts b/src/core/server/legacy/plugins/index.ts deleted file mode 100644 index 7ec5dbc1983ab..0000000000000 --- a/src/core/server/legacy/plugins/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { findLegacyPluginSpecs } from './find_legacy_plugin_specs'; -export { logLegacyThirdPartyPluginDeprecationWarning } from './log_legacy_plugins_warning'; diff --git a/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts b/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts deleted file mode 100644 index 2317f1036ce42..0000000000000 --- a/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { loggerMock } from '../../logging/logger.mock'; -import { logLegacyThirdPartyPluginDeprecationWarning } from './log_legacy_plugins_warning'; -import { LegacyPluginSpec } from '../types'; - -const createPluginSpec = ({ id, path }: { id: string; path: string }): LegacyPluginSpec => { - return { - getId: () => id, - getExpectedKibanaVersion: () => 'kibana', - getConfigPrefix: () => 'plugin.config', - getPack: () => ({ - getPath: () => path, - }), - }; -}; - -describe('logLegacyThirdPartyPluginDeprecationWarning', () => { - let log: ReturnType; - - beforeEach(() => { - log = loggerMock.create(); - }); - - it('logs warning for third party plugins', () => { - logLegacyThirdPartyPluginDeprecationWarning({ - specs: [createPluginSpec({ id: 'plugin', path: '/some-external-path' })], - log, - }); - expect(log.warn).toHaveBeenCalledTimes(1); - expect(log.warn.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "Some installed third party plugin(s) [plugin] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://ela.st/kibana-breaking-changes-8-0 for a list of breaking changes and https://ela.st/kibana-platform-migration for documentation on how to migrate legacy plugins.", - ] - `); - }); - - it('lists all the deprecated plugins and only log once', () => { - logLegacyThirdPartyPluginDeprecationWarning({ - specs: [ - createPluginSpec({ id: 'pluginA', path: '/abs/path/to/pluginA' }), - createPluginSpec({ id: 'pluginB', path: '/abs/path/to/pluginB' }), - createPluginSpec({ id: 'pluginC', path: '/abs/path/to/pluginC' }), - ], - log, - }); - expect(log.warn).toHaveBeenCalledTimes(1); - expect(log.warn.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "Some installed third party plugin(s) [pluginA, pluginB, pluginC] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://ela.st/kibana-breaking-changes-8-0 for a list of breaking changes and https://ela.st/kibana-platform-migration for documentation on how to migrate legacy plugins.", - ] - `); - }); - - it('does not log warning for internal legacy plugins', () => { - logLegacyThirdPartyPluginDeprecationWarning({ - specs: [ - createPluginSpec({ - id: 'plugin', - path: '/absolute/path/to/kibana/src/legacy/core_plugins', - }), - createPluginSpec({ - id: 'plugin', - path: '/absolute/path/to/kibana/x-pack', - }), - ], - log, - }); - - expect(log.warn).not.toHaveBeenCalled(); - }); -}); diff --git a/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts b/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts deleted file mode 100644 index 4a4a1b1b0e60b..0000000000000 --- a/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Logger } from '../../logging'; -import { LegacyPluginSpec } from '../types'; - -const internalPaths = ['/src/legacy/core_plugins', '/x-pack']; - -// Use shortened URLs so destinations can be updated if/when documentation moves -// All platform team members have access to edit these -const breakingChangesUrl = 'https://ela.st/kibana-breaking-changes-8-0'; -const migrationGuideUrl = 'https://ela.st/kibana-platform-migration'; - -export const logLegacyThirdPartyPluginDeprecationWarning = ({ - specs, - log, -}: { - specs: LegacyPluginSpec[]; - log: Logger; -}) => { - const thirdPartySpecs = specs.filter(isThirdPartyPluginSpec); - if (thirdPartySpecs.length > 0) { - const pluginIds = thirdPartySpecs.map((spec) => spec.getId()); - log.warn( - `Some installed third party plugin(s) [${pluginIds.join( - ', ' - )}] are using the legacy plugin format and will no longer work in a future Kibana release. ` + - `Please refer to ${breakingChangesUrl} for a list of breaking changes ` + - `and ${migrationGuideUrl} for documentation on how to migrate legacy plugins.` - ); - } -}; - -const isThirdPartyPluginSpec = (spec: LegacyPluginSpec): boolean => { - const pluginPath = spec.getPack().getPath(); - return !internalPaths.some((internalPath) => pluginPath.indexOf(internalPath) > -1); -}; diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts index 1105308fd44cf..12bfddfff1961 100644 --- a/src/core/server/legacy/types.ts +++ b/src/core/server/legacy/types.ts @@ -17,10 +17,6 @@ * under the License. */ -import { Server } from 'hapi'; - -import { ChromeNavLink } from '../../public'; -import { KibanaRequest, LegacyRequest } from '../http'; import { InternalCoreSetup, InternalCoreStart } from '../internal_types'; import { PluginsServiceSetup, PluginsServiceStart, UiPlugins } from '../plugins'; import { InternalRenderingServiceSetup } from '../rendering'; @@ -50,91 +46,6 @@ export interface LegacyConfig { set(config: LegacyVars): void; } -/** - * @internal - * @deprecated - */ -export interface LegacyPluginPack { - getPath(): string; -} - -/** - * @internal - * @deprecated - */ -export interface LegacyPluginSpec { - getId: () => unknown; - getExpectedKibanaVersion: () => string; - getConfigPrefix: () => string; - getPack: () => LegacyPluginPack; -} - -/** - * @internal - * @deprecated - */ -export interface VarsProvider { - fn: (server: Server, configValue: any) => LegacyVars; - pluginSpec: { - readConfigValue(config: any, key: string | string[]): any; - }; -} - -/** - * @internal - * @deprecated - */ -export type VarsInjector = () => LegacyVars; - -/** - * @internal - * @deprecated - */ -export type VarsReplacer = ( - vars: LegacyVars, - request: LegacyRequest, - server: Server -) => LegacyVars | Promise; - -/** - * @internal - * @deprecated - */ -export type LegacyNavLinkSpec = Partial & { - id: string; - title: string; - url: string; -}; - -/** - * @internal - * @deprecated - */ -export type LegacyAppSpec = Partial & { - pluginId?: string; - listed?: boolean; -}; - -/** - * @internal - * @deprecated - */ -export type LegacyNavLink = Omit & { - order: number; -}; - -/** - * @internal - * @deprecated - */ -export interface LegacyUiExports { - defaultInjectedVarProviders?: VarsProvider[]; - injectedVarsReplacers?: VarsReplacer[]; - navLinkSpecs?: LegacyNavLinkSpec[] | null; - uiAppSpecs?: Array; - unknown?: [{ pluginSpec: LegacyPluginSpec; type: unknown }]; -} - /** * @public * @deprecated @@ -158,43 +69,7 @@ export interface LegacyServiceStartDeps { * @internal * @deprecated */ -export interface ILegacyInternals { - /** - * Inject UI app vars for a particular plugin - */ - injectUiAppVars(id: string, injector: VarsInjector): void; - - /** - * Get all the merged injected UI app vars for a particular plugin - */ - getInjectedUiAppVars(id: string): Promise; - - /** - * Get the metadata vars for a particular plugin - */ - getVars( - id: string, - request: KibanaRequest | LegacyRequest, - injected?: LegacyVars - ): Promise; -} - -/** - * @internal - * @deprecated - */ -export interface LegacyPlugins { - disabledPluginSpecs: LegacyPluginSpec[]; - pluginSpecs: LegacyPluginSpec[]; - uiExports: LegacyUiExports; - navLinks: LegacyNavLink[]; -} - -/** - * @internal - * @deprecated - */ -export interface LegacyServiceDiscoverPlugins extends LegacyPlugins { - pluginExtendedConfig: LegacyConfig; +export interface LegacyServiceSetupConfig { + legacyConfig: LegacyConfig; settings: LegacyVars; } diff --git a/src/core/server/rendering/__mocks__/params.ts b/src/core/server/rendering/__mocks__/params.ts index 0901cec768cd2..ae3830f703a53 100644 --- a/src/core/server/rendering/__mocks__/params.ts +++ b/src/core/server/rendering/__mocks__/params.ts @@ -20,19 +20,16 @@ import { mockCoreContext } from '../../core_context.mock'; import { httpServiceMock } from '../../http/http_service.mock'; import { pluginServiceMock } from '../../plugins/plugins_service.mock'; -import { legacyServiceMock } from '../../legacy/legacy_service.mock'; import { statusServiceMock } from '../../status/status_service.mock'; const context = mockCoreContext.create(); const http = httpServiceMock.createInternalSetupContract(); const uiPlugins = pluginServiceMock.createUiPlugins(); -const legacyPlugins = legacyServiceMock.createDiscoverPlugins(); const status = statusServiceMock.createInternalSetupContract(); export const mockRenderingServiceParams = context; export const mockRenderingSetupDeps = { http, - legacyPlugins, uiPlugins, status, }; diff --git a/src/core/server/rendering/__mocks__/rendering_service.ts b/src/core/server/rendering/__mocks__/rendering_service.ts index 179a09b8619b0..01d084f9ae53c 100644 --- a/src/core/server/rendering/__mocks__/rendering_service.ts +++ b/src/core/server/rendering/__mocks__/rendering_service.ts @@ -27,11 +27,9 @@ export const setupMock: jest.Mocked = { render: jest.fn(), }; export const mockSetup = jest.fn().mockResolvedValue(setupMock); -export const mockStart = jest.fn(); export const mockStop = jest.fn(); export const mockRenderingService: jest.Mocked = { setup: mockSetup, - start: mockStart, stop: mockStop, }; export const RenderingService = jest.fn( diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index ab828a1780425..07ca59a48c6b0 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -27,15 +27,6 @@ Object { "translationsUrl": "/mock-server-basepath/translations/en.json", }, "legacyMetadata": Object { - "app": Object {}, - "basePath": "/mock-server-basepath", - "branch": Any, - "buildNum": Any, - "buildSha": Any, - "bundleId": "app:core", - "devMode": true, - "nav": Array [], - "serverName": "http-server-test", "uiSettings": Object { "defaults": Object { "registered": Object { @@ -44,7 +35,6 @@ Object { }, "user": Object {}, }, - "version": Any, }, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], @@ -80,15 +70,6 @@ Object { "translationsUrl": "/mock-server-basepath/translations/en.json", }, "legacyMetadata": Object { - "app": Object {}, - "basePath": "/mock-server-basepath", - "branch": Any, - "buildNum": Any, - "buildSha": Any, - "bundleId": "app:core", - "devMode": true, - "nav": Array [], - "serverName": "http-server-test", "uiSettings": Object { "defaults": Object { "registered": Object { @@ -97,7 +78,6 @@ Object { }, "user": Object {}, }, - "version": Any, }, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], @@ -133,15 +113,6 @@ Object { "translationsUrl": "/mock-server-basepath/translations/en.json", }, "legacyMetadata": Object { - "app": Object {}, - "basePath": "/mock-server-basepath", - "branch": Any, - "buildNum": Any, - "buildSha": Any, - "bundleId": "app:core", - "devMode": true, - "nav": Array [], - "serverName": "http-server-test", "uiSettings": Object { "defaults": Object { "registered": Object { @@ -154,7 +125,6 @@ Object { }, }, }, - "version": Any, }, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], @@ -190,15 +160,6 @@ Object { "translationsUrl": "/translations/en.json", }, "legacyMetadata": Object { - "app": Object {}, - "basePath": "", - "branch": Any, - "buildNum": Any, - "buildSha": Any, - "bundleId": "app:core", - "devMode": true, - "nav": Array [], - "serverName": "http-server-test", "uiSettings": Object { "defaults": Object { "registered": Object { @@ -207,7 +168,6 @@ Object { }, "user": Object {}, }, - "version": Any, }, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], @@ -243,15 +203,6 @@ Object { "translationsUrl": "/mock-server-basepath/translations/en.json", }, "legacyMetadata": Object { - "app": Object {}, - "basePath": "/mock-server-basepath", - "branch": Any, - "buildNum": Any, - "buildSha": Any, - "bundleId": "app:core", - "devMode": true, - "nav": Array [], - "serverName": "http-server-test", "uiSettings": Object { "defaults": Object { "registered": Object { @@ -260,7 +211,6 @@ Object { }, "user": Object {}, }, - "version": Any, }, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], diff --git a/src/core/server/rendering/rendering_service.test.ts b/src/core/server/rendering/rendering_service.test.ts index 254bafed5b194..08978cd1df64d 100644 --- a/src/core/server/rendering/rendering_service.test.ts +++ b/src/core/server/rendering/rendering_service.test.ts @@ -43,12 +43,6 @@ const INJECTED_METADATA = { version: expect.any(String), }, }, - legacyMetadata: { - branch: expect.any(String), - buildNum: expect.any(Number), - buildSha: expect.any(String), - version: expect.any(String), - }, }; const { createKibanaRequest, createRawRequest } = httpServerMock; @@ -72,13 +66,6 @@ describe('RenderingService', () => { registered: { name: 'title' }, }); render = (await service.setup(mockRenderingSetupDeps)).render; - await service.start({ - legacy: { - legacyInternals: { - getVars: () => ({}), - }, - }, - } as any); }); it('renders "core" page', async () => { diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index 7761c89044f6f..738787f940905 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -20,14 +20,11 @@ import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { take } from 'rxjs/operators'; - import { i18n } from '@kbn/i18n'; import { UiPlugins } from '../plugins'; -import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { Template } from './views'; -import { LegacyService } from '../legacy'; import { IRenderOptions, RenderingSetupDeps, @@ -36,25 +33,20 @@ import { } from './types'; /** @internal */ -export class RenderingService implements CoreService { - private legacyInternals?: LegacyService['legacyInternals']; +export class RenderingService { constructor(private readonly coreContext: CoreContext) {} public async setup({ http, status, - legacyPlugins, uiPlugins, }: RenderingSetupDeps): Promise { return { render: async ( request, uiSettings, - { app = { getId: () => 'core' }, includeUserSettings = true, vars }: IRenderOptions = {} + { includeUserSettings = true, vars }: IRenderOptions = {} ) => { - if (!this.legacyInternals) { - throw new Error('Cannot render before "start"'); - } const env = { mode: this.coreContext.env.mode, packageInfo: this.coreContext.env.packageInfo, @@ -65,7 +57,6 @@ export class RenderingService implements CoreService ({ id, @@ -96,16 +87,6 @@ export class RenderingService implements CoreService; }>; legacyMetadata: { - app: { getId(): string }; - bundleId: string; - nav: LegacyNavLink[]; - version: string; - branch: string; - buildNum: number; - buildSha: string; - serverName: string; - devMode: boolean; - basePath: string; uiSettings: { defaults: Record; user: Record>; @@ -78,7 +67,6 @@ export interface RenderingMetadata { /** @internal */ export interface RenderingSetupDeps { http: InternalHttpServiceSetup; - legacyPlugins: LegacyServiceDiscoverPlugins; status: InternalStatusServiceSetup; uiPlugins: UiPlugins; } @@ -91,14 +79,6 @@ export interface IRenderOptions { */ includeUserSettings?: boolean; - /** - * Render the bootstrapped HTML content for an optional legacy application. - * Defaults to `core`. - * @deprecated for legacy use only, remove with ui_render_mixin - * @internal - */ - app?: { getId(): string }; - /** * Inject custom vars into the page metadata. * @deprecated for legacy use only, remove with ui_render_mixin diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 8a764d9bd2f66..cc51d27589ce7 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -864,10 +864,6 @@ export interface IndexSettingsDeprecationInfo { // @public (undocumented) export interface IRenderOptions { - // @internal @deprecated - app?: { - getId(): string; - }; includeUserSettings?: boolean; // @internal @deprecated vars?: Record; @@ -1286,21 +1282,6 @@ export class LegacyElasticsearchErrorHelpers { static isNotAuthorizedError(error: any): error is LegacyElasticsearchError; } -// Warning: (ae-forgotten-export) The symbol "ILegacyInternals" needs to be exported by the entry point index.d.ts -// -// @internal @deprecated (undocumented) -export class LegacyInternals implements ILegacyInternals { - constructor(uiExports: LegacyUiExports, config: LegacyConfig, server: Server); - // (undocumented) - getInjectedUiAppVars(id: string): Promise>; - // (undocumented) - getVars(id: string, request: KibanaRequest | LegacyRequest, injected?: LegacyVars): Promise>; - // Warning: (ae-forgotten-export) The symbol "VarsInjector" needs to be exported by the entry point index.d.ts - // - // (undocumented) - injectUiAppVars(id: string, injector: VarsInjector): void; - } - // @public @deprecated (undocumented) export interface LegacyRequest extends Request { } @@ -1312,16 +1293,6 @@ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { callAsInternalUser(endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; } -// Warning: (ae-forgotten-export) The symbol "LegacyPlugins" needs to be exported by the entry point index.d.ts -// -// @internal @deprecated (undocumented) -export interface LegacyServiceDiscoverPlugins extends LegacyPlugins { - // (undocumented) - pluginExtendedConfig: LegacyConfig; - // (undocumented) - settings: LegacyVars; -} - // @public @deprecated (undocumented) export interface LegacyServiceSetupDeps { // Warning: (ae-forgotten-export) The symbol "LegacyCoreSetup" needs to be exported by the entry point index.d.ts @@ -1346,31 +1317,6 @@ export interface LegacyServiceStartDeps { plugins: Record; } -// @internal @deprecated (undocumented) -export interface LegacyUiExports { - // Warning: (ae-forgotten-export) The symbol "VarsProvider" needs to be exported by the entry point index.d.ts - // - // (undocumented) - defaultInjectedVarProviders?: VarsProvider[]; - // Warning: (ae-forgotten-export) The symbol "VarsReplacer" needs to be exported by the entry point index.d.ts - // - // (undocumented) - injectedVarsReplacers?: VarsReplacer[]; - // Warning: (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts - // - // (undocumented) - navLinkSpecs?: LegacyNavLinkSpec[] | null; - // Warning: (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts - // - // (undocumented) - uiAppSpecs?: Array; - // (undocumented) - unknown?: [{ - pluginSpec: LegacyPluginSpec; - type: unknown; - }]; -} - // Warning: (ae-forgotten-export) The symbol "lifecycleResponseFactory" needs to be exported by the entry point index.d.ts // // @public @@ -2734,7 +2680,6 @@ export const validBodyOutput: readonly ["data", "stream"]; // Warnings were encountered during analysis: // // src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:135:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:274:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:274:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:277:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 8502f563cb0c2..5935636d54f9d 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -113,11 +113,12 @@ export class Server { const { pluginTree, uiPlugins } = await this.plugins.discover({ environment: environmentSetup, }); - const legacyPlugins = await this.legacy.discoverPlugins(); + const legacyConfigSetup = await this.legacy.setupLegacyConfig(); // Immediately terminate in case of invalid configuration + // This needs to be done after plugin discovery await this.configService.validate(); - await ensureValidConfiguration(this.configService, legacyPlugins); + await ensureValidConfiguration(this.configService, legacyConfigSetup); const contextServiceSetup = this.context.setup({ // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: @@ -166,7 +167,6 @@ export class Server { const renderingSetup = await this.rendering.setup({ http: httpSetup, status: statusSetup, - legacyPlugins, uiPlugins, }); @@ -248,10 +248,6 @@ export class Server { await this.http.start(); - await this.rendering.start({ - legacy: this.legacy, - }); - return this.coreStart; } diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 5d31db63773fa..3c556a4f1ba3c 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -55,7 +55,6 @@ export default { '@elastic/eui$': '/node_modules/@elastic/eui/test-env', '@elastic/eui/lib/(.*)?': '/node_modules/@elastic/eui/test-env/$1', '^src/plugins/(.*)': '/src/plugins/$1', - '^uiExports/(.*)': '/src/dev/jest/mocks/file_mock.js', '^test_utils/(.*)': '/src/test_utils/public/$1', '^fixtures/(.*)': '/src/fixtures/$1', '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': diff --git a/src/legacy/plugin_discovery/README.md b/src/legacy/plugin_discovery/README.md deleted file mode 100644 index 83e7c10d16fff..0000000000000 --- a/src/legacy/plugin_discovery/README.md +++ /dev/null @@ -1,148 +0,0 @@ -# Plugin Discovery - -The plugin discovery module defines the core plugin loading logic used by the Kibana server. It exports functions for - - -## `findPluginSpecs(settings, [config])` - -Finds [`PluginSpec`][PluginSpec] objects - -### params - - `settings`: the same settings object accepted by [`KbnServer`][KbnServer] - - `[config]`: Optional - a [`Config`][Config] service. Using this param causes `findPluginSpecs()` to modify `config`'s schema to support the configuration for each discovered [`PluginSpec`][PluginSpec]. If you can, please use the [`Config`][Config] service produced by `extendedConfig$` rather than passing in an existing service so that `findPluginSpecs()` is side-effect free. - -### return value - -`findPluginSpecs()` returns an object of Observables which produce values at different parts of the process. Since the Observables are all aware of their own dependencies you can subscribe to any combination (within the same tick) and only the necessary plugin logic will be executed. - -If you *never* subscribe to any of the Observables then plugin discovery won't actually run. - - - `pack$`: emits every [`PluginPack`][PluginPack] found - - `invalidDirectoryError$: Observable`: emits [`InvalidDirectoryError`][Errors]s caused by `settings.plugins.scanDirs` values that don't point to actual directories. `findPluginSpecs()` will not abort when this error is encountered. - - `invalidPackError$: Observable`: emits [`InvalidPackError`][Errors]s caused by children of `settings.plugins.scanDirs` or `settings.plugins.paths` values which don't meet the requirements of a [`PluginPack`][PluginPack] (probably missing a `package.json`). `findPluginSpecs()` will not abort when this error is encountered. - - `deprecation$: Observable`: emits deprecation warnings that are produces when reading each [`PluginPack`][PluginPack]'s configuration - - `extendedConfig$: Observable`: emits the [`Config`][Config] service that was passed to `findPluginSpecs()` (or created internally if none was passed) after it has been extended with the configuration from each plugin - - `spec$: Observable`: emits every *enabled* [`PluginSpec`][PluginSpec] defined by the discovered [`PluginPack`][PluginPack]s - - `disabledSpec$: Observable`: emits every *disabled* [`PluginSpec`][PluginSpec] defined by the discovered [`PluginPack`][PluginPack]s - - `invalidVersionSpec$: Observable`: emits every [`PluginSpec`][PluginSpec] who's required kibana version does not match the version exposed by `config.get('pkg.version')` - -### example - -Just get the plugin specs, only fail if there is an uncaught error of some sort: -```js -const { pack$ } = findPluginSpecs(settings); -const packs = await pack$.pipe(toArray()).toPromise() -``` - -Just log the deprecation messages: -```js -const { deprecation$ } = findPluginSpecs(settings); -for (const warning of await deprecation$.pipe(toArray()).toPromise()) { - console.log('DEPRECATION:', warning) -} -``` - -Get the packs but fail if any packs are invalid: -```js -const { pack$, invalidDirectoryError$ } = findPluginSpecs(settings); -const packs = await Rx.merge( - pack$.pipe(toArray()), - - // if we ever get an InvalidDirectoryError, throw it - // into the stream so that all streams are unsubscribed, - // the discovery process is aborted, and the promise rejects - invalidDirectoryError$.pipe( - map(error => { throw error }) - ), -).toPromise() -``` - -Handle everything -```js -const { - pack$, - invalidDirectoryError$, - invalidPackError$, - deprecation$, - extendedConfig$, - spec$, - disabledSpecs$, - invalidVersionSpec$, -} = findPluginSpecs(settings); - -Rx.merge( - pack$.pipe( - tap(pluginPack => console.log('Found plugin pack', pluginPack)) - ), - - invalidDirectoryError$.pipe( - tap(error => console.log('Invalid directory error', error)) - ), - - invalidPackError$.pipe( - tap(error => console.log('Invalid plugin pack error', error)) - ), - - deprecation$.pipe( - tap(msg => console.log('DEPRECATION:', msg)) - ), - - extendedConfig$.pipe( - tap(config => console.log('config service extended by plugins', config)) - ), - - spec$.pipe( - tap(pluginSpec => console.log('enabled plugin spec found', spec)) - ), - - disabledSpec$.pipe( - tap(pluginSpec => console.log('disabled plugin spec found', spec)) - ), - - invalidVersionSpec$.pipe( - tap(pluginSpec => console.log('plugin spec with invalid version found', spec)) - ), -) -.toPromise() -.then(() => { - console.log('plugin discovery complete') -}) -.catch((error) => { - console.log('plugin discovery failed', error) -}) - -``` - -## `reduceExportSpecs(pluginSpecs, reducers, [defaults={}])` - -Reduces every value exported by the [`PluginSpec`][PluginSpec]s to produce a single value. If an exported value is an array each item in the array will be reduced individually. If the exported value is `undefined` it will be ignored. The reducer is called with the signature: - -```js -reducer( - // the result of the previous reducer call, or `defaults` - acc: any, - // the exported value, found at `uiExports[type]` or `uiExports[type][i]` - // in the PluginSpec config. - spec: any, - // the key in `uiExports` where this export was found - type: string, - // the PluginSpec which exported this spec - pluginSpec: PluginSpec -) -``` - -## `new PluginPack(options)` class - -Only exported so that `PluginPack` instances can be created in tests and used in place of on-disk plugin fixtures. Use `findPluginSpecs()`, or the cached result of a call to `findPluginSpecs()` (like `kbnServer.pluginSpecs`) any time you might need access to `PluginPack` objects in distributed code. - -### params - - - `options.path`: absolute path to where this plugin pack was found, this is normally a direct child of `./src/legacy/core_plugins` or `./plugins` - - `options.pkg`: the parsed `package.json` for this pack, used for defaults in `PluginSpec` objects defined by this pack - - `options.provider`: the default export of the pack, a function which is called with the `PluginSpec` class which should return one or more `PluginSpec` objects. - -[PluginPack]: ./plugin_pack/plugin_pack.js "PluginPath class definition" -[PluginSpec]: ./plugin_spec/plugin_spec.js "PluginSpec class definition" -[Errors]: ./errors.js "PluginDiscover specific error types" -[KbnServer]: ../server/kbn_server.js "KbnServer class definition" -[Config]: ../server/config/config.js "KbnServer/Config class definition" diff --git a/src/legacy/plugin_discovery/__tests__/find_plugin_specs.js b/src/legacy/plugin_discovery/__tests__/find_plugin_specs.js deleted file mode 100644 index e6af23d69c549..0000000000000 --- a/src/legacy/plugin_discovery/__tests__/find_plugin_specs.js +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve } from 'path'; -import { toArray } from 'rxjs/operators'; - -import expect from '@kbn/expect'; -import { isEqual } from 'lodash'; -import { findPluginSpecs } from '../find_plugin_specs'; -import { PluginSpec } from '../plugin_spec'; - -const PLUGIN_FIXTURES = resolve(__dirname, 'fixtures/plugins'); -const CONFLICT_FIXTURES = resolve(__dirname, 'fixtures/conflicts'); - -describe('plugin discovery', () => { - describe('findPluginSpecs()', function () { - this.timeout(10000); - - describe('spec$', () => { - it('finds specs for specified plugin paths', async () => { - const { spec$ } = findPluginSpecs({ - plugins: { - paths: [ - resolve(PLUGIN_FIXTURES, 'foo'), - resolve(PLUGIN_FIXTURES, 'bar'), - resolve(PLUGIN_FIXTURES, 'broken'), - ], - }, - }); - - const specs = await spec$.pipe(toArray()).toPromise(); - expect(specs).to.have.length(3); - specs.forEach((spec) => { - expect(spec).to.be.a(PluginSpec); - }); - expect(specs.map((s) => s.getId()).sort()).to.eql(['bar:one', 'bar:two', 'foo']); - }); - - it('finds all specs in scanDirs', async () => { - const { spec$ } = findPluginSpecs({ - // used to ensure the dev_mode plugin is enabled - env: 'development', - - plugins: { - scanDirs: [PLUGIN_FIXTURES], - }, - }); - - const specs = await spec$.pipe(toArray()).toPromise(); - expect(specs).to.have.length(3); - specs.forEach((spec) => { - expect(spec).to.be.a(PluginSpec); - }); - expect(specs.map((s) => s.getId()).sort()).to.eql(['bar:one', 'bar:two', 'foo']); - }); - - it('does not find disabled plugins', async () => { - const { spec$ } = findPluginSpecs({ - 'bar:one': { - enabled: false, - }, - - plugins: { - paths: [ - resolve(PLUGIN_FIXTURES, 'foo'), - resolve(PLUGIN_FIXTURES, 'bar'), - resolve(PLUGIN_FIXTURES, 'broken'), - ], - }, - }); - - const specs = await spec$.pipe(toArray()).toPromise(); - expect(specs).to.have.length(2); - specs.forEach((spec) => { - expect(spec).to.be.a(PluginSpec); - }); - expect(specs.map((s) => s.getId()).sort()).to.eql(['bar:two', 'foo']); - }); - - it('dedupes duplicate packs', async () => { - const { spec$ } = findPluginSpecs({ - plugins: { - scanDirs: [PLUGIN_FIXTURES], - paths: [ - resolve(PLUGIN_FIXTURES, 'foo'), - resolve(PLUGIN_FIXTURES, 'foo'), - resolve(PLUGIN_FIXTURES, 'bar'), - resolve(PLUGIN_FIXTURES, 'bar'), - resolve(PLUGIN_FIXTURES, 'broken'), - resolve(PLUGIN_FIXTURES, 'broken'), - ], - }, - }); - - const specs = await spec$.pipe(toArray()).toPromise(); - expect(specs).to.have.length(3); - specs.forEach((spec) => { - expect(spec).to.be.a(PluginSpec); - }); - expect(specs.map((s) => s.getId()).sort()).to.eql(['bar:one', 'bar:two', 'foo']); - }); - - describe('conflicting plugin spec ids', () => { - it('fails with informative message', async () => { - const { spec$ } = findPluginSpecs({ - plugins: { - scanDirs: [], - paths: [resolve(CONFLICT_FIXTURES, 'foo')], - }, - }); - - try { - await spec$.pipe(toArray()).toPromise(); - throw new Error('expected spec$ to throw an error'); - } catch (error) { - expect(error.message).to.contain('Multiple plugins found with the id "foo"'); - expect(error.message).to.contain(CONFLICT_FIXTURES); - } - }); - }); - }); - - describe('packageJson$', () => { - const checkPackageJsons = (packageJsons) => { - expect(packageJsons).to.have.length(2); - const package1 = packageJsons.find((packageJson) => - isEqual( - { - directoryPath: resolve(PLUGIN_FIXTURES, 'foo'), - contents: { - name: 'foo', - version: 'kibana', - }, - }, - packageJson - ) - ); - expect(package1).to.be.an(Object); - const package2 = packageJsons.find((packageJson) => - isEqual( - { - directoryPath: resolve(PLUGIN_FIXTURES, 'bar'), - contents: { - name: 'foo', - version: 'kibana', - }, - }, - packageJson - ) - ); - expect(package2).to.be.an(Object); - }; - - it('finds packageJson for specified plugin paths', async () => { - const { packageJson$ } = findPluginSpecs({ - plugins: { - paths: [ - resolve(PLUGIN_FIXTURES, 'foo'), - resolve(PLUGIN_FIXTURES, 'bar'), - resolve(PLUGIN_FIXTURES, 'broken'), - ], - }, - }); - - const packageJsons = await packageJson$.pipe(toArray()).toPromise(); - checkPackageJsons(packageJsons); - }); - - it('finds all packageJsons in scanDirs', async () => { - const { packageJson$ } = findPluginSpecs({ - // used to ensure the dev_mode plugin is enabled - env: 'development', - - plugins: { - scanDirs: [PLUGIN_FIXTURES], - }, - }); - - const packageJsons = await packageJson$.pipe(toArray()).toPromise(); - checkPackageJsons(packageJsons); - }); - - it('dedupes duplicate packageJson', async () => { - const { packageJson$ } = findPluginSpecs({ - plugins: { - scanDirs: [PLUGIN_FIXTURES], - paths: [ - resolve(PLUGIN_FIXTURES, 'foo'), - resolve(PLUGIN_FIXTURES, 'foo'), - resolve(PLUGIN_FIXTURES, 'bar'), - resolve(PLUGIN_FIXTURES, 'bar'), - resolve(PLUGIN_FIXTURES, 'broken'), - resolve(PLUGIN_FIXTURES, 'broken'), - ], - }, - }); - - const packageJsons = await packageJson$.pipe(toArray()).toPromise(); - checkPackageJsons(packageJsons); - }); - }); - }); -}); diff --git a/src/legacy/plugin_discovery/__tests__/fixtures/conflicts/foo/index.js b/src/legacy/plugin_discovery/__tests__/fixtures/conflicts/foo/index.js deleted file mode 100644 index fcbe3487463b7..0000000000000 --- a/src/legacy/plugin_discovery/__tests__/fixtures/conflicts/foo/index.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export default function (kibana) { - return [ - // two plugins exported without ids will both inherit - // the id of the pack and conflict - new kibana.Plugin({}), - new kibana.Plugin({}), - ]; -} diff --git a/src/legacy/plugin_discovery/__tests__/fixtures/conflicts/foo/package.json b/src/legacy/plugin_discovery/__tests__/fixtures/conflicts/foo/package.json deleted file mode 100644 index e43c2f0bc984c..0000000000000 --- a/src/legacy/plugin_discovery/__tests__/fixtures/conflicts/foo/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "foo", - "version": "kibana" -} diff --git a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/bar/index.js b/src/legacy/plugin_discovery/__tests__/fixtures/plugins/bar/index.js deleted file mode 100644 index 0eef126f2255a..0000000000000 --- a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/bar/index.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export default function (kibana) { - return [ - new kibana.Plugin({ - id: 'bar:one', - }), - new kibana.Plugin({ - id: 'bar:two', - }), - ]; -} diff --git a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/bar/package.json b/src/legacy/plugin_discovery/__tests__/fixtures/plugins/bar/package.json deleted file mode 100644 index e43c2f0bc984c..0000000000000 --- a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/bar/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "foo", - "version": "kibana" -} diff --git a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/broken/index.js b/src/legacy/plugin_discovery/__tests__/fixtures/plugins/broken/index.js deleted file mode 100644 index 59f4a2649f019..0000000000000 --- a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/broken/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export default { - foo: 'bar', -}; diff --git a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/broken/package1.json b/src/legacy/plugin_discovery/__tests__/fixtures/plugins/broken/package1.json deleted file mode 100644 index 81ddb6221d515..0000000000000 --- a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/broken/package1.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "baz", - "version": "kibana" -} diff --git a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/foo/index.js b/src/legacy/plugin_discovery/__tests__/fixtures/plugins/foo/index.js deleted file mode 100644 index e43a1dcedb372..0000000000000 --- a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/foo/index.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -module.exports = function (kibana) { - return new kibana.Plugin({ - id: 'foo', - }); -}; diff --git a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/foo/package.json b/src/legacy/plugin_discovery/__tests__/fixtures/plugins/foo/package.json deleted file mode 100644 index e43c2f0bc984c..0000000000000 --- a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/foo/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "foo", - "version": "kibana" -} diff --git a/src/legacy/plugin_discovery/errors.js b/src/legacy/plugin_discovery/errors.js deleted file mode 100644 index 02d81b32d1fd1..0000000000000 --- a/src/legacy/plugin_discovery/errors.js +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const errorCodeProperty = Symbol('pluginDiscovery/errorCode'); - -/** - * Thrown when reading a plugin directory fails, wraps failure - * @type {String} - */ -const ERROR_INVALID_DIRECTORY = 'ERROR_INVALID_DIRECTORY'; -export function createInvalidDirectoryError(sourceError, path) { - sourceError[errorCodeProperty] = ERROR_INVALID_DIRECTORY; - sourceError.path = path; - return sourceError; -} -export function isInvalidDirectoryError(error) { - return error && error[errorCodeProperty] === ERROR_INVALID_DIRECTORY; -} - -/** - * Thrown when trying to create a PluginPack for a path that - * is not a valid plugin definition - * @type {String} - */ -const ERROR_INVALID_PACK = 'ERROR_INVALID_PACK'; -export function createInvalidPackError(path, reason) { - const error = new Error(`PluginPack${path ? ` at "${path}"` : ''} ${reason}`); - error[errorCodeProperty] = ERROR_INVALID_PACK; - error.path = path; - return error; -} -export function isInvalidPackError(error) { - return error && error[errorCodeProperty] === ERROR_INVALID_PACK; -} - -/** - * Thrown when trying to load a PluginSpec that is invalid for some reason - * @type {String} - */ -const ERROR_INVALID_PLUGIN = 'ERROR_INVALID_PLUGIN'; -export function createInvalidPluginError(spec, reason) { - const error = new Error( - `Plugin from ${spec.getId()} at ${spec.getPack().getPath()} is invalid because ${reason}` - ); - error[errorCodeProperty] = ERROR_INVALID_PLUGIN; - error.spec = spec; - return error; -} -export function isInvalidPluginError(error) { - return error && error[errorCodeProperty] === ERROR_INVALID_PLUGIN; -} - -/** - * Thrown when trying to load a PluginSpec whose version is incompatible - * @type {String} - */ -const ERROR_INCOMPATIBLE_PLUGIN_VERSION = 'ERROR_INCOMPATIBLE_PLUGIN_VERSION'; -export function createIncompatiblePluginVersionError(spec) { - const error = new Error( - `Plugin ${spec.getId()} is only compatible with Kibana version ${spec.getExpectedKibanaVersion()}` - ); - error[errorCodeProperty] = ERROR_INCOMPATIBLE_PLUGIN_VERSION; - error.spec = spec; - return error; -} -export function isIncompatiblePluginVersionError(error) { - return error && error[errorCodeProperty] === ERROR_INCOMPATIBLE_PLUGIN_VERSION; -} diff --git a/src/legacy/plugin_discovery/find_plugin_specs.js b/src/legacy/plugin_discovery/find_plugin_specs.js deleted file mode 100644 index b97476bb456a5..0000000000000 --- a/src/legacy/plugin_discovery/find_plugin_specs.js +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import * as Rx from 'rxjs'; -import { - distinct, - toArray, - mergeMap, - share, - shareReplay, - filter, - last, - map, - tap, -} from 'rxjs/operators'; -import { realpathSync } from 'fs'; - -import { Config } from '../server/config'; - -import { extendConfigService, disableConfigExtension } from './plugin_config'; - -import { - createPack$, - createPackageJsonAtPath$, - createPackageJsonsInDirectory$, -} from './plugin_pack'; - -import { isInvalidDirectoryError, isInvalidPackError } from './errors'; - -export function defaultConfig(settings) { - return Config.withDefaultSchema(settings); -} - -function bufferAllResults(observable) { - return observable.pipe( - // buffer all results into a single array - toArray(), - // merge the array back into the stream when complete - mergeMap((array) => array) - ); -} - -/** - * Determine a distinct value for each result from find$ - * so they can be deduplicated - * @param {{error?,pack?}} result - * @return {Any} - */ -function getDistinctKeyForFindResult(result) { - // errors are distinct by their message - if (result.error) { - return result.error.message; - } - - // packs are distinct by their absolute and real path - if (result.packageJson) { - return realpathSync(result.packageJson.directoryPath); - } - - // non error/pack results shouldn't exist, but if they do they are all unique - return result; -} - -function groupSpecsById(specs) { - const specsById = new Map(); - for (const spec of specs) { - const id = spec.getId(); - if (specsById.has(id)) { - specsById.get(id).push(spec); - } else { - specsById.set(id, [spec]); - } - } - return specsById; -} - -/** - * Creates a collection of observables for discovering pluginSpecs - * using Kibana's defaults, settings, and config service - * - * @param {Object} settings - * @param {ConfigService} [configToMutate] when supplied **it is mutated** to - * include the config from discovered plugin specs - * @return {Object} - */ -export function findPluginSpecs(settings, configToMutate) { - const config$ = Rx.defer(async () => { - if (configToMutate) { - return configToMutate; - } - - return defaultConfig(settings); - }).pipe(shareReplay()); - - // find plugin packs in configured paths/dirs - const packageJson$ = config$.pipe( - mergeMap((config) => - Rx.merge( - ...config.get('plugins.paths').map(createPackageJsonAtPath$), - ...config.get('plugins.scanDirs').map(createPackageJsonsInDirectory$) - ) - ), - distinct(getDistinctKeyForFindResult), - share() - ); - - const pack$ = createPack$(packageJson$).pipe(share()); - - const extendConfig$ = config$.pipe( - mergeMap((config) => - pack$.pipe( - // get the specs for each found plugin pack - mergeMap(({ pack }) => (pack ? pack.getPluginSpecs() : [])), - // make sure that none of the plugin specs have conflicting ids, fail - // early if conflicts detected or merge the specs back into the stream - toArray(), - mergeMap((allSpecs) => { - for (const [id, specs] of groupSpecsById(allSpecs)) { - if (specs.length > 1) { - throw new Error( - `Multiple plugins found with the id "${id}":\n${specs - .map((spec) => ` - ${id} at ${spec.getPath()}`) - .join('\n')}` - ); - } - } - - return allSpecs; - }), - mergeMap(async (spec) => { - // extend the config service with this plugin spec and - // collect its deprecations messages if some of its - // settings are outdated - const deprecations = []; - await extendConfigService(spec, config, settings, (message) => { - deprecations.push({ spec, message }); - }); - - return { - spec, - deprecations, - }; - }), - // extend the config with all plugins before determining enabled status - bufferAllResults, - map(({ spec, deprecations }) => { - const isRightVersion = spec.isVersionCompatible(config.get('pkg.version')); - const enabled = isRightVersion && spec.isEnabled(config); - return { - config, - spec, - deprecations, - enabledSpecs: enabled ? [spec] : [], - disabledSpecs: enabled ? [] : [spec], - invalidVersionSpecs: isRightVersion ? [] : [spec], - }; - }), - // determine which plugins are disabled before actually removing things from the config - bufferAllResults, - tap((result) => { - for (const spec of result.disabledSpecs) { - disableConfigExtension(spec, config); - } - }) - ) - ), - share() - ); - - return { - // package JSONs found when searching configure paths - packageJson$: packageJson$.pipe( - mergeMap((result) => (result.packageJson ? [result.packageJson] : [])) - ), - - // plugin packs found when searching configured paths - pack$: pack$.pipe(mergeMap((result) => (result.pack ? [result.pack] : []))), - - // errors caused by invalid directories of plugin directories - invalidDirectoryError$: pack$.pipe( - mergeMap((result) => (isInvalidDirectoryError(result.error) ? [result.error] : [])) - ), - - // errors caused by directories that we expected to be plugin but were invalid - invalidPackError$: pack$.pipe( - mergeMap((result) => (isInvalidPackError(result.error) ? [result.error] : [])) - ), - - otherError$: pack$.pipe( - mergeMap((result) => (isUnhandledError(result.error) ? [result.error] : [])) - ), - - // { spec, message } objects produced when transforming deprecated - // settings for a plugin spec - deprecation$: extendConfig$.pipe(mergeMap((result) => result.deprecations)), - - // the config service we extended with all of the plugin specs, - // only emitted once it is fully extended by all - extendedConfig$: extendConfig$.pipe( - mergeMap((result) => result.config), - filter(Boolean), - last() - ), - - // all enabled PluginSpec objects - spec$: extendConfig$.pipe(mergeMap((result) => result.enabledSpecs)), - - // all disabled PluginSpec objects - disabledSpec$: extendConfig$.pipe(mergeMap((result) => result.disabledSpecs)), - - // all PluginSpec objects that were disabled because their version was incompatible - invalidVersionSpec$: extendConfig$.pipe(mergeMap((result) => result.invalidVersionSpecs)), - }; -} - -function isUnhandledError(error) { - return error != null && !isInvalidDirectoryError(error) && !isInvalidPackError(error); -} diff --git a/src/legacy/plugin_discovery/index.js b/src/legacy/plugin_discovery/index.js deleted file mode 100644 index b60806f6cbc23..0000000000000 --- a/src/legacy/plugin_discovery/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { findPluginSpecs } from './find_plugin_specs'; -export { reduceExportSpecs } from './plugin_exports'; -export { PluginPack } from './plugin_pack'; diff --git a/src/legacy/plugin_discovery/plugin_config/__tests__/extend_config_service.js b/src/legacy/plugin_discovery/plugin_config/__tests__/extend_config_service.js deleted file mode 100644 index 40f84f6f54b3b..0000000000000 --- a/src/legacy/plugin_discovery/plugin_config/__tests__/extend_config_service.js +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import sinon from 'sinon'; -import expect from '@kbn/expect'; - -import { Config } from '../../../server/config'; -import { PluginPack } from '../../plugin_pack'; -import { extendConfigService, disableConfigExtension } from '../extend_config_service'; -import * as SchemaNS from '../schema'; -import * as SettingsNS from '../settings'; - -describe('plugin discovery/extend config service', () => { - const sandbox = sinon.createSandbox(); - afterEach(() => sandbox.restore()); - - const pluginSpec = new PluginPack({ - path: '/dev/null', - pkg: { - name: 'test', - version: 'kibana', - }, - provider: ({ Plugin }) => - new Plugin({ - configPrefix: 'foo.bar.baz', - - config: (Joi) => - Joi.object({ - enabled: Joi.boolean().default(true), - test: Joi.string().default('bonk'), - }).default(), - }), - }) - .getPluginSpecs() - .pop(); - - describe('extendConfigService()', () => { - it('calls getSettings, getSchema, and Config.extendSchema() correctly', async () => { - const rootSettings = { - foo: { - bar: { - enabled: false, - }, - }, - }; - const schema = { - validate: () => {}, - }; - const configPrefix = 'foo.bar'; - const config = { - extendSchema: sandbox.stub(), - }; - const pluginSpec = { - getConfigPrefix: sandbox.stub().returns(configPrefix), - }; - - const getSettings = sandbox.stub(SettingsNS, 'getSettings').returns(rootSettings.foo.bar); - - const getSchema = sandbox.stub(SchemaNS, 'getSchema').returns(schema); - - await extendConfigService(pluginSpec, config, rootSettings); - - sinon.assert.calledOnce(getSettings); - sinon.assert.calledWithExactly(getSettings, pluginSpec, rootSettings); - - sinon.assert.calledOnce(getSchema); - sinon.assert.calledWithExactly(getSchema, pluginSpec); - - sinon.assert.calledOnce(config.extendSchema); - sinon.assert.calledWithExactly( - config.extendSchema, - schema, - rootSettings.foo.bar, - configPrefix - ); - }); - - it('adds the schema for a plugin spec to its config prefix', async () => { - const config = Config.withDefaultSchema(); - expect(config.has('foo.bar.baz')).to.be(false); - await extendConfigService(pluginSpec, config); - expect(config.has('foo.bar.baz')).to.be(true); - }); - - it('initializes it with the default settings', async () => { - const config = Config.withDefaultSchema(); - await extendConfigService(pluginSpec, config); - expect(config.get('foo.bar.baz.enabled')).to.be(true); - expect(config.get('foo.bar.baz.test')).to.be('bonk'); - }); - - it('initializes it with values from root settings if defined', async () => { - const config = Config.withDefaultSchema(); - await extendConfigService(pluginSpec, config, { - foo: { - bar: { - baz: { - test: 'hello world', - }, - }, - }, - }); - - expect(config.get('foo.bar.baz.test')).to.be('hello world'); - }); - - it('throws if root settings are invalid', async () => { - const config = Config.withDefaultSchema(); - try { - await extendConfigService(pluginSpec, config, { - foo: { - bar: { - baz: { - test: { - 'not a string': true, - }, - }, - }, - }, - }); - throw new Error('Expected extendConfigService() to throw because of bad settings'); - } catch (error) { - expect(error.message).to.contain('"test" must be a string'); - } - }); - }); - - describe('disableConfigExtension()', () => { - it('removes added config', async () => { - const config = Config.withDefaultSchema(); - await extendConfigService(pluginSpec, config); - expect(config.has('foo.bar.baz.test')).to.be(true); - await disableConfigExtension(pluginSpec, config); - expect(config.has('foo.bar.baz.test')).to.be(false); - }); - - it('leaves {configPrefix}.enabled config', async () => { - const config = Config.withDefaultSchema(); - expect(config.has('foo.bar.baz.enabled')).to.be(false); - await extendConfigService(pluginSpec, config); - expect(config.get('foo.bar.baz.enabled')).to.be(true); - await disableConfigExtension(pluginSpec, config); - expect(config.get('foo.bar.baz.enabled')).to.be(false); - }); - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_config/__tests__/schema.js b/src/legacy/plugin_discovery/plugin_config/__tests__/schema.js deleted file mode 100644 index 78adb1e680e20..0000000000000 --- a/src/legacy/plugin_discovery/plugin_config/__tests__/schema.js +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; - -import { PluginPack } from '../../plugin_pack'; -import { getSchema, getStubSchema } from '../schema'; - -describe('plugin discovery/schema', () => { - function createPluginSpec(configProvider) { - return new PluginPack({ - path: '/dev/null', - pkg: { - name: 'test', - version: 'kibana', - }, - provider: ({ Plugin }) => - new Plugin({ - configPrefix: 'foo.bar.baz', - config: configProvider, - }), - }) - .getPluginSpecs() - .pop(); - } - - describe('getSchema()', () => { - it('calls the config provider and returns its return value', async () => { - const pluginSpec = createPluginSpec(() => 'foo'); - expect(await getSchema(pluginSpec)).to.be('foo'); - }); - - it('supports config provider that returns a promise', async () => { - const pluginSpec = createPluginSpec(() => Promise.resolve('foo')); - expect(await getSchema(pluginSpec)).to.be('foo'); - }); - - it('uses default schema when no config provider', async () => { - const schema = await getSchema(createPluginSpec()); - expect(schema).to.be.an('object'); - expect(schema).to.have.property('validate').a('function'); - expect(schema.validate({}).value).to.eql({ - enabled: true, - }); - }); - - it('uses default schema when config returns falsy value', async () => { - const schema = await getSchema(createPluginSpec(() => null)); - expect(schema).to.be.an('object'); - expect(schema).to.have.property('validate').a('function'); - expect(schema.validate({}).value).to.eql({ - enabled: true, - }); - }); - - it('uses default schema when config promise resolves to falsy value', async () => { - const schema = await getSchema(createPluginSpec(() => Promise.resolve(null))); - expect(schema).to.be.an('object'); - expect(schema).to.have.property('validate').a('function'); - expect(schema.validate({}).value).to.eql({ - enabled: true, - }); - }); - }); - - describe('getStubSchema()', () => { - it('returns schema with enabled: false', async () => { - const schema = await getStubSchema(); - expect(schema).to.be.an('object'); - expect(schema).to.have.property('validate').a('function'); - expect(schema.validate({}).value).to.eql({ - enabled: false, - }); - }); - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_config/__tests__/settings.js b/src/legacy/plugin_discovery/plugin_config/__tests__/settings.js deleted file mode 100644 index 750c5ee6c6f50..0000000000000 --- a/src/legacy/plugin_discovery/plugin_config/__tests__/settings.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; - -import { PluginPack } from '../../plugin_pack'; -import { getSettings } from '../settings'; - -describe('plugin_discovery/settings', () => { - const pluginSpec = new PluginPack({ - path: '/dev/null', - pkg: { - name: 'test', - version: 'kibana', - }, - provider: ({ Plugin }) => - new Plugin({ - configPrefix: 'a.b.c', - }), - }) - .getPluginSpecs() - .pop(); - - describe('getSettings()', () => { - it('reads settings from config prefix', async () => { - const rootSettings = { - a: { - b: { - c: { - enabled: false, - }, - }, - }, - }; - - expect(await getSettings(pluginSpec, rootSettings)).to.eql({ - enabled: false, - }); - }); - - it('allows rootSettings to be undefined', async () => { - expect(await getSettings(pluginSpec)).to.eql(undefined); - }); - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_config/extend_config_service.js b/src/legacy/plugin_discovery/plugin_config/extend_config_service.js deleted file mode 100644 index a6d5d4ae5f990..0000000000000 --- a/src/legacy/plugin_discovery/plugin_config/extend_config_service.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { getSettings } from './settings'; -import { getSchema, getStubSchema } from './schema'; - -/** - * Extend a config service with the schema and settings for a - * plugin spec and optionally call logDeprecation with warning - * messages about deprecated settings that are used - * @param {PluginSpec} spec - * @param {Server.Config} config - * @param {Object} rootSettings - * @param {Function} [logDeprecation] - * @return {Promise} - */ -export async function extendConfigService(spec, config, rootSettings) { - const settings = await getSettings(spec, rootSettings); - const schema = await getSchema(spec); - config.extendSchema(schema, settings, spec.getConfigPrefix()); -} - -/** - * Disable the schema and settings applied to a config service for - * a plugin spec - * @param {PluginSpec} spec - * @param {Server.Config} config - * @return {undefined} - */ -export function disableConfigExtension(spec, config) { - const prefix = spec.getConfigPrefix(); - config.removeSchema(prefix); - config.extendSchema(getStubSchema(), { enabled: false }, prefix); -} diff --git a/src/legacy/plugin_discovery/plugin_config/index.js b/src/legacy/plugin_discovery/plugin_config/index.js deleted file mode 100644 index a27463bc9c7f5..0000000000000 --- a/src/legacy/plugin_discovery/plugin_config/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { extendConfigService, disableConfigExtension } from './extend_config_service'; diff --git a/src/legacy/plugin_discovery/plugin_config/schema.js b/src/legacy/plugin_discovery/plugin_config/schema.js deleted file mode 100644 index 14d10aa5568da..0000000000000 --- a/src/legacy/plugin_discovery/plugin_config/schema.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Joi from 'joi'; - -const STUB_CONFIG_SCHEMA = Joi.object() - .keys({ - enabled: Joi.valid(false).default(false), - }) - .default(); - -const DEFAULT_CONFIG_SCHEMA = Joi.object() - .keys({ - enabled: Joi.boolean().default(true), - }) - .default(); - -/** - * Get the config schema for a plugin spec - * @param {PluginSpec} spec - * @return {Promise} - */ -export async function getSchema(spec) { - const provider = spec.getConfigSchemaProvider(); - return (provider && (await provider(Joi))) || DEFAULT_CONFIG_SCHEMA; -} - -export function getStubSchema() { - return STUB_CONFIG_SCHEMA; -} diff --git a/src/legacy/plugin_discovery/plugin_config/settings.js b/src/legacy/plugin_discovery/plugin_config/settings.js deleted file mode 100644 index e6a4741d76eca..0000000000000 --- a/src/legacy/plugin_discovery/plugin_config/settings.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { get } from 'lodash'; - -/** - * Get the settings for a pluginSpec from the raw root settings while - * optionally calling logDeprecation() with warnings about deprecated - * settings that were used - * @param {PluginSpec} spec - * @param {Object} rootSettings - * @return {Promise} - */ -export async function getSettings(spec, rootSettings) { - const prefix = spec.getConfigPrefix(); - const rawSettings = get(rootSettings, prefix); - return rawSettings; -} diff --git a/src/legacy/plugin_discovery/plugin_exports/__tests__/reduce_export_specs.js b/src/legacy/plugin_discovery/plugin_exports/__tests__/reduce_export_specs.js deleted file mode 100644 index 3beaacc1a8293..0000000000000 --- a/src/legacy/plugin_discovery/plugin_exports/__tests__/reduce_export_specs.js +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; - -import { PluginPack } from '../../plugin_pack'; -import { reduceExportSpecs } from '../reduce_export_specs'; - -const PLUGIN = new PluginPack({ - path: __dirname, - pkg: { - name: 'foo', - version: 'kibana', - }, - provider: ({ Plugin }) => - new Plugin({ - uiExports: { - concatNames: { - name: 'export1', - }, - - concat: ['export2', 'export3'], - }, - }), -}); - -const REDUCERS = { - concatNames(acc, spec, type, pluginSpec) { - return { - names: [].concat(acc.names || [], `${pluginSpec.getId()}:${spec.name}`), - }; - }, - concat(acc, spec, type, pluginSpec) { - return { - names: [].concat(acc.names || [], `${pluginSpec.getId()}:${spec}`), - }; - }, -}; - -const PLUGIN_SPECS = PLUGIN.getPluginSpecs(); - -describe('reduceExportSpecs', () => { - it('combines ui exports from a list of plugin definitions', () => { - const exports = reduceExportSpecs(PLUGIN_SPECS, REDUCERS); - expect(exports).to.eql({ - names: ['foo:export1', 'foo:export2', 'foo:export3'], - }); - }); - - it('starts with the defaults', () => { - const exports = reduceExportSpecs(PLUGIN_SPECS, REDUCERS, { - names: ['default'], - }); - - expect(exports).to.eql({ - names: ['default', 'foo:export1', 'foo:export2', 'foo:export3'], - }); - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_exports/index.js b/src/legacy/plugin_discovery/plugin_exports/index.js deleted file mode 100644 index 0e3511ea85dd4..0000000000000 --- a/src/legacy/plugin_discovery/plugin_exports/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { reduceExportSpecs } from './reduce_export_specs'; diff --git a/src/legacy/plugin_discovery/plugin_exports/reduce_export_specs.js b/src/legacy/plugin_discovery/plugin_exports/reduce_export_specs.js deleted file mode 100644 index a3adc3091085d..0000000000000 --- a/src/legacy/plugin_discovery/plugin_exports/reduce_export_specs.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Combine the exportSpecs from a list of pluginSpecs - * by calling the reducers for each export type - * @param {Array} pluginSpecs - * @param {Object} reducers - * @param {Object} [defaults={}] - * @return {Object} - */ -export function reduceExportSpecs(pluginSpecs, reducers, defaults = {}) { - return pluginSpecs.reduce((acc, pluginSpec) => { - const specsByType = pluginSpec.getExportSpecs() || {}; - const types = Object.keys(specsByType); - - return types.reduce((acc, type) => { - const reducer = reducers[type] || reducers.unknown; - - if (!reducer) { - throw new Error(`Unknown export type ${type}`); - } - - // convert specs to an array if not already one or - // ignore the spec if it is undefined - const specs = [].concat(specsByType[type] === undefined ? [] : specsByType[type]); - - return specs.reduce((acc, spec) => reducer(acc, spec, type, pluginSpec), acc); - }, acc); - }, defaults); -} diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/create_pack.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/create_pack.js deleted file mode 100644 index b17bd69479ffa..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/create_pack.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve } from 'path'; -import * as Rx from 'rxjs'; -import { toArray } from 'rxjs/operators'; -import expect from '@kbn/expect'; - -import { createPack$ } from '../create_pack'; -import { PluginPack } from '../plugin_pack'; - -import { PLUGINS_DIR, assertInvalidPackError } from './utils'; - -describe('plugin discovery/create pack', () => { - it('creates PluginPack', async () => { - const packageJson$ = Rx.from([ - { - packageJson: { - directoryPath: resolve(PLUGINS_DIR, 'prebuilt'), - contents: { - name: 'prebuilt', - }, - }, - }, - ]); - const results = await createPack$(packageJson$).pipe(toArray()).toPromise(); - expect(results).to.have.length(1); - expect(results[0]).to.only.have.keys(['pack']); - const { pack } = results[0]; - expect(pack).to.be.a(PluginPack); - }); - - describe('errors thrown', () => { - async function checkError(path, check) { - const packageJson$ = Rx.from([ - { - packageJson: { - directoryPath: path, - }, - }, - ]); - - const results = await createPack$(packageJson$).pipe(toArray()).toPromise(); - expect(results).to.have.length(1); - expect(results[0]).to.only.have.keys(['error']); - const { error } = results[0]; - await check(error); - } - it('default export is an object', () => - checkError(resolve(PLUGINS_DIR, 'exports_object'), (error) => { - assertInvalidPackError(error); - expect(error.message).to.contain('must export a function'); - })); - it('default export is an number', () => - checkError(resolve(PLUGINS_DIR, 'exports_number'), (error) => { - assertInvalidPackError(error); - expect(error.message).to.contain('must export a function'); - })); - it('default export is an string', () => - checkError(resolve(PLUGINS_DIR, 'exports_string'), (error) => { - assertInvalidPackError(error); - expect(error.message).to.contain('must export a function'); - })); - it('directory with code that fails when required', () => - checkError(resolve(PLUGINS_DIR, 'broken_code'), (error) => { - expect(error.message).to.contain("Cannot find module 'does-not-exist'"); - })); - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken/package.json b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken/package.json deleted file mode 100644 index f830e8b60c02d..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": -} diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken_code/index.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken_code/index.js deleted file mode 100644 index bdb26504d6b6e..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken_code/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const brokenRequire = require('does-not-exist'); // eslint-disable-line - -module.exports = function (kibana) { - return new kibana.Plugin({ - id: 'foo', - }); -}; diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken_code/package.json b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken_code/package.json deleted file mode 100644 index e43c2f0bc984c..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken_code/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "foo", - "version": "kibana" -} diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/index.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/index.js deleted file mode 100644 index f24fc54e38d9a..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export default 1; diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/package.json b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/package.json deleted file mode 100644 index e43c2f0bc984c..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "foo", - "version": "kibana" -} diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/index.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/index.js deleted file mode 100644 index 59f4a2649f019..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export default { - foo: 'bar', -}; diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/package.json b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/package.json deleted file mode 100644 index e43c2f0bc984c..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "foo", - "version": "kibana" -} diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/index.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/index.js deleted file mode 100644 index 8900db15321ae..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export default 'foo'; diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/package.json b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/package.json deleted file mode 100644 index e43c2f0bc984c..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "foo", - "version": "kibana" -} diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/index.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/index.js deleted file mode 100644 index e43a1dcedb372..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/index.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -module.exports = function (kibana) { - return new kibana.Plugin({ - id: 'foo', - }); -}; diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/package.json b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/package.json deleted file mode 100644 index e43c2f0bc984c..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "foo", - "version": "kibana" -} diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/index.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/index.js deleted file mode 100644 index edb1dd15673da..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -console.log('hello world'); diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/index.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/index.js deleted file mode 100644 index 050ffdfbde9ea..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { myLib } from './my_lib'; diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/my_lib.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/my_lib.js deleted file mode 100644 index 94e511632d9a6..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/my_lib.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export function myLib() { - console.log('lib'); -} diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/index.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/index.js deleted file mode 100644 index 89744b2dd3fd9..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/index.js +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint-disable */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -exports.default = function (_ref) { - var Plugin = _ref.Plugin; - - return new Plugin({ - id: 'foo' - }); -}; diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/package.json b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/package.json deleted file mode 100644 index b1b74e0e76b12..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "prebuilt" -} diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/package_json_at_path.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/package_json_at_path.js deleted file mode 100644 index fa1033180954e..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/package_json_at_path.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve } from 'path'; -import { toArray } from 'rxjs/operators'; - -import expect from '@kbn/expect'; - -import { createPackageJsonAtPath$ } from '../package_json_at_path'; -import { PLUGINS_DIR, assertInvalidPackError, assertInvalidDirectoryError } from './utils'; - -describe('plugin discovery/plugin_pack', () => { - describe('createPackageJsonAtPath$()', () => { - it('returns an observable', () => { - expect(createPackageJsonAtPath$()).to.have.property('subscribe').a('function'); - }); - it('gets the default provider from prebuilt babel modules', async () => { - const results = await createPackageJsonAtPath$(resolve(PLUGINS_DIR, 'prebuilt')) - .pipe(toArray()) - .toPromise(); - expect(results).to.have.length(1); - expect(results[0]).to.only.have.keys(['packageJson']); - expect(results[0].packageJson).to.be.an(Object); - expect(results[0].packageJson.directoryPath).to.be(resolve(PLUGINS_DIR, 'prebuilt')); - expect(results[0].packageJson.contents).to.eql({ name: 'prebuilt' }); - }); - describe('errors emitted as { error } results', () => { - async function checkError(path, check) { - const results = await createPackageJsonAtPath$(path).pipe(toArray()).toPromise(); - expect(results).to.have.length(1); - expect(results[0]).to.only.have.keys(['error']); - const { error } = results[0]; - await check(error); - } - it('undefined path', () => - checkError(undefined, (error) => { - assertInvalidDirectoryError(error); - expect(error.message).to.contain('path must be a string'); - })); - it('relative path', () => - checkError('plugins/foo', (error) => { - assertInvalidDirectoryError(error); - expect(error.message).to.contain('path must be absolute'); - })); - it('./relative path', () => - checkError('./plugins/foo', (error) => { - assertInvalidDirectoryError(error); - expect(error.message).to.contain('path must be absolute'); - })); - it('non-existent path', () => - checkError(resolve(PLUGINS_DIR, 'baz'), (error) => { - assertInvalidPackError(error); - expect(error.message).to.contain('must be a directory'); - })); - it('path to a file', () => - checkError(resolve(PLUGINS_DIR, 'index.js'), (error) => { - assertInvalidPackError(error); - expect(error.message).to.contain('must be a directory'); - })); - it('directory without a package.json', () => - checkError(resolve(PLUGINS_DIR, 'lib'), (error) => { - assertInvalidPackError(error); - expect(error.message).to.contain('must have a package.json file'); - })); - it('directory with an invalid package.json', () => - checkError(resolve(PLUGINS_DIR, 'broken'), (error) => { - assertInvalidPackError(error); - expect(error.message).to.contain('must have a valid package.json file'); - })); - }); - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/package_jsons_in_directory.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/package_jsons_in_directory.js deleted file mode 100644 index 37cb4cc064da7..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/package_jsons_in_directory.js +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve } from 'path'; - -import { toArray } from 'rxjs/operators'; -import expect from '@kbn/expect'; - -import { createPackageJsonsInDirectory$ } from '../package_jsons_in_directory'; - -import { PLUGINS_DIR, assertInvalidDirectoryError } from './utils'; - -describe('plugin discovery/packs in directory', () => { - describe('createPackageJsonsInDirectory$()', () => { - describe('errors emitted as { error } results', () => { - async function checkError(path, check) { - const results = await createPackageJsonsInDirectory$(path).pipe(toArray()).toPromise(); - expect(results).to.have.length(1); - expect(results[0]).to.only.have.keys('error'); - const { error } = results[0]; - await check(error); - } - - it('undefined path', () => - checkError(undefined, (error) => { - assertInvalidDirectoryError(error); - expect(error.message).to.contain('path must be a string'); - })); - it('relative path', () => - checkError('my/plugins', (error) => { - assertInvalidDirectoryError(error); - expect(error.message).to.contain('path must be absolute'); - })); - it('./relative path', () => - checkError('./my/pluginsd', (error) => { - assertInvalidDirectoryError(error); - expect(error.message).to.contain('path must be absolute'); - })); - it('non-existent path', () => - checkError(resolve(PLUGINS_DIR, 'notreal'), (error) => { - assertInvalidDirectoryError(error); - expect(error.message).to.contain('no such file or directory'); - })); - it('path to a file', () => - checkError(resolve(PLUGINS_DIR, 'index.js'), (error) => { - assertInvalidDirectoryError(error); - expect(error.message).to.contain('not a directory'); - })); - }); - - it('includes child errors for invalid packageJsons within a valid directory', async () => { - const results = await createPackageJsonsInDirectory$(PLUGINS_DIR).pipe(toArray()).toPromise(); - - const errors = results.map((result) => result.error).filter(Boolean); - - const packageJsons = results.map((result) => result.packageJson).filter(Boolean); - - packageJsons.forEach((pack) => expect(pack).to.be.an(Object)); - // there should be one result for each item in PLUGINS_DIR - expect(results).to.have.length(8); - // three of the fixtures are errors of some sort - expect(errors).to.have.length(2); - // six of them are valid - expect(packageJsons).to.have.length(6); - }); - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/plugin_pack.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/plugin_pack.js deleted file mode 100644 index 769fcd74ce6fb..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/plugin_pack.js +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -import { PluginPack } from '../plugin_pack'; -import { PluginSpec } from '../../plugin_spec'; - -describe('plugin discovery/plugin pack', () => { - describe('constructor', () => { - it('requires an object', () => { - expect(() => { - new PluginPack(); - }).to.throwError(); - }); - }); - describe('#getPkg()', () => { - it('returns the `pkg` constructor argument', () => { - const pkg = {}; - const pack = new PluginPack({ pkg }); - expect(pack.getPkg()).to.be(pkg); - }); - }); - describe('#getPath()', () => { - it('returns the `path` constructor argument', () => { - const path = {}; - const pack = new PluginPack({ path }); - expect(pack.getPath()).to.be(path); - }); - }); - describe('#getPluginSpecs()', () => { - it('calls the `provider` constructor argument with an api including a single sub class of PluginSpec', () => { - const provider = sinon.stub(); - const pack = new PluginPack({ provider }); - sinon.assert.notCalled(provider); - pack.getPluginSpecs(); - sinon.assert.calledOnce(provider); - sinon.assert.calledWithExactly(provider, { - Plugin: sinon.match((Class) => { - return Class.prototype instanceof PluginSpec; - }, 'Subclass of PluginSpec'), - }); - }); - - it('casts undefined return value to array', () => { - const pack = new PluginPack({ provider: () => undefined }); - expect(pack.getPluginSpecs()).to.eql([]); - }); - - it('casts single PluginSpec to an array', () => { - const pack = new PluginPack({ - path: '/dev/null', - pkg: { name: 'foo', version: 'kibana' }, - provider: ({ Plugin }) => new Plugin({}), - }); - - const specs = pack.getPluginSpecs(); - expect(specs).to.be.an('array'); - expect(specs).to.have.length(1); - expect(specs[0]).to.be.a(PluginSpec); - }); - - it('returns an array of PluginSpec', () => { - const pack = new PluginPack({ - path: '/dev/null', - pkg: { name: 'foo', version: 'kibana' }, - provider: ({ Plugin }) => [new Plugin({}), new Plugin({})], - }); - - const specs = pack.getPluginSpecs(); - expect(specs).to.be.an('array'); - expect(specs).to.have.length(2); - expect(specs[0]).to.be.a(PluginSpec); - expect(specs[1]).to.be.a(PluginSpec); - }); - - it('throws if non-undefined return value is not an instance of api.Plugin', () => { - let OtherPluginSpecClass; - const otherPack = new PluginPack({ - path: '/dev/null', - pkg: { name: 'foo', version: 'kibana' }, - provider: (api) => { - OtherPluginSpecClass = api.Plugin; - }, - }); - - // call getPluginSpecs() on other pack to get it's api.Plugin class - otherPack.getPluginSpecs(); - - const badPacks = [ - new PluginPack({ provider: () => false }), - new PluginPack({ provider: () => null }), - new PluginPack({ provider: () => 1 }), - new PluginPack({ provider: () => 'true' }), - new PluginPack({ provider: () => true }), - new PluginPack({ provider: () => new Date() }), - new PluginPack({ provider: () => /foo.*bar/ }), - new PluginPack({ provider: () => function () {} }), - new PluginPack({ provider: () => new OtherPluginSpecClass({}) }), - ]; - - for (const pack of badPacks) { - expect(() => pack.getPluginSpecs()).to.throwError((error) => { - expect(error.message).to.contain('unexpected plugin export'); - }); - } - }); - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/utils.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/utils.js deleted file mode 100644 index adcf60d809ff7..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/utils.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve } from 'path'; -import { inspect } from 'util'; - -import { isInvalidPackError, isInvalidDirectoryError } from '../../errors'; - -export const PLUGINS_DIR = resolve(__dirname, 'fixtures/plugins'); - -export function assertInvalidDirectoryError(error) { - if (!isInvalidDirectoryError(error)) { - throw new Error(`Expected ${inspect(error)} to be an 'InvalidDirectoryError'`); - } -} - -export function assertInvalidPackError(error) { - if (!isInvalidPackError(error)) { - throw new Error(`Expected ${inspect(error)} to be an 'InvalidPackError'`); - } -} diff --git a/src/legacy/plugin_discovery/plugin_pack/create_pack.js b/src/legacy/plugin_discovery/plugin_pack/create_pack.js deleted file mode 100644 index 189c2ea324103..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/create_pack.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { PluginPack } from './plugin_pack'; -import { map, catchError } from 'rxjs/operators'; -import { createInvalidPackError } from '../errors'; - -function createPack(packageJson) { - let provider = require(packageJson.directoryPath); // eslint-disable-line import/no-dynamic-require - if (provider.__esModule) { - provider = provider.default; - } - if (typeof provider !== 'function') { - throw createInvalidPackError(packageJson.directoryPath, 'must export a function'); - } - - return new PluginPack({ path: packageJson.directoryPath, pkg: packageJson.contents, provider }); -} - -export const createPack$ = (packageJson$) => - packageJson$.pipe( - map(({ error, packageJson }) => { - if (error) { - return { error }; - } - - if (!packageJson) { - throw new Error('packageJson is required to create the pack'); - } - - return { - pack: createPack(packageJson), - }; - }), - // createPack can throw errors, and we want them to be represented - // like the errors we consume from createPackageJsonAtPath/Directory - catchError((error) => [{ error }]) - ); diff --git a/src/legacy/plugin_discovery/plugin_pack/index.js b/src/legacy/plugin_discovery/plugin_pack/index.js deleted file mode 100644 index 69e55baee660b..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/index.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { createPack$ } from './create_pack'; -export { createPackageJsonAtPath$ } from './package_json_at_path'; -export { createPackageJsonsInDirectory$ } from './package_jsons_in_directory'; -export { PluginPack } from './plugin_pack'; diff --git a/src/legacy/plugin_discovery/plugin_pack/lib/fs.js b/src/legacy/plugin_discovery/plugin_pack/lib/fs.js deleted file mode 100644 index 2b531e314df52..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/lib/fs.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { stat, readdir } from 'fs'; -import { resolve, isAbsolute } from 'path'; - -import { fromNode as fcb } from 'bluebird'; -import * as Rx from 'rxjs'; -import { catchError, mergeAll, filter, map, mergeMap } from 'rxjs/operators'; - -import { createInvalidDirectoryError } from '../../errors'; - -function assertAbsolutePath(path) { - if (typeof path !== 'string') { - throw createInvalidDirectoryError(new TypeError('path must be a string'), path); - } - - if (!isAbsolute(path)) { - throw createInvalidDirectoryError(new TypeError('path must be absolute'), path); - } -} - -async function statTest(path, test) { - try { - const stats = await fcb((cb) => stat(path, cb)); - return Boolean(test(stats)); - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } - } - return false; -} - -/** - * Determine if a path currently points to a directory - * @param {String} path - * @return {Promise} - */ -export async function isDirectory(path) { - assertAbsolutePath(path); - return await statTest(path, (stat) => stat.isDirectory()); -} - -/** - * Get absolute paths for child directories within a path - * @param {string} path - * @return {Promise>} - */ -export const createChildDirectory$ = (path) => - Rx.defer(() => { - assertAbsolutePath(path); - return fcb((cb) => readdir(path, cb)); - }).pipe( - catchError((error) => { - throw createInvalidDirectoryError(error, path); - }), - mergeAll(), - filter((name) => !name.startsWith('.')), - map((name) => resolve(path, name)), - mergeMap(async (absolute) => { - if (await isDirectory(absolute)) { - return [absolute]; - } else { - return []; - } - }), - mergeAll() - ); diff --git a/src/legacy/plugin_discovery/plugin_pack/lib/index.js b/src/legacy/plugin_discovery/plugin_pack/lib/index.js deleted file mode 100644 index 491deeda27516..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/lib/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { isDirectory, createChildDirectory$ } from './fs'; diff --git a/src/legacy/plugin_discovery/plugin_pack/package_json_at_path.js b/src/legacy/plugin_discovery/plugin_pack/package_json_at_path.js deleted file mode 100644 index 18629ef3ea802..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/package_json_at_path.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { readFileSync } from 'fs'; -import * as Rx from 'rxjs'; -import { map, mergeMap, catchError } from 'rxjs/operators'; -import { resolve } from 'path'; -import { createInvalidPackError } from '../errors'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { isNewPlatformPlugin } from '../../../core/server/plugins'; - -import { isDirectory } from './lib'; - -async function createPackageJsonAtPath(path) { - if (!(await isDirectory(path))) { - throw createInvalidPackError(path, 'must be a directory'); - } - - let str; - try { - str = readFileSync(resolve(path, 'package.json')); - } catch (err) { - throw createInvalidPackError(path, 'must have a package.json file'); - } - - let pkg; - try { - pkg = JSON.parse(str); - } catch (err) { - throw createInvalidPackError(path, 'must have a valid package.json file'); - } - - return { - directoryPath: path, - contents: pkg, - }; -} - -export const createPackageJsonAtPath$ = (path) => - // If plugin directory contains manifest file, we should skip it since it - // should have been handled by the core plugin system already. - Rx.defer(() => isNewPlatformPlugin(path)).pipe( - mergeMap((isNewPlatformPlugin) => (isNewPlatformPlugin ? [] : createPackageJsonAtPath(path))), - map((packageJson) => ({ packageJson })), - catchError((error) => [{ error }]) - ); diff --git a/src/legacy/plugin_discovery/plugin_pack/package_jsons_in_directory.js b/src/legacy/plugin_discovery/plugin_pack/package_jsons_in_directory.js deleted file mode 100644 index 5f0977f4829b8..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/package_jsons_in_directory.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { mergeMap, catchError } from 'rxjs/operators'; -import { isInvalidDirectoryError } from '../errors'; - -import { createChildDirectory$ } from './lib'; -import { createPackageJsonAtPath$ } from './package_json_at_path'; - -/** - * Finds the plugins within a directory. Results are - * an array of objects with either `pack` or `error` - * keys. - * - * - `{ error }` results are provided when the path is not - * a directory, or one of the child directories is not a - * valid plugin pack. - * - `{ pack }` results are for discovered plugins defs - * - * @param {String} path - * @return {Array<{pack}|{error}>} - */ -export const createPackageJsonsInDirectory$ = (path) => - createChildDirectory$(path).pipe( - mergeMap(createPackageJsonAtPath$), - catchError((error) => { - // this error is produced by createChildDirectory$() when the path - // is invalid, we return them as an error result similar to how - // createPackAtPath$ works when it finds invalid packs in a directory - if (isInvalidDirectoryError(error)) { - return [{ error }]; - } - - throw error; - }) - ); diff --git a/src/legacy/plugin_discovery/plugin_pack/plugin_pack.js b/src/legacy/plugin_discovery/plugin_pack/plugin_pack.js deleted file mode 100644 index 1baf3d104ca84..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/plugin_pack.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { inspect } from 'util'; - -import { PluginSpec } from '../plugin_spec'; - -export class PluginPack { - constructor({ path, pkg, provider }) { - this._path = path; - this._pkg = pkg; - this._provider = provider; - } - - /** - * Get the contents of this plugin pack's package.json file - * @return {Object} - */ - getPkg() { - return this._pkg; - } - - /** - * Get the absolute path to this plugin pack on disk - * @return {String} - */ - getPath() { - return this._path; - } - - /** - * Invoke the plugin pack's provider to get the list - * of specs defined in this plugin. - * @return {Array} - */ - getPluginSpecs() { - const pack = this; - const api = { - Plugin: class ScopedPluginSpec extends PluginSpec { - constructor(options) { - super(pack, options); - } - }, - }; - - const result = this._provider(api); - const specs = [].concat(result === undefined ? [] : result); - - // verify that all specs are instances of passed "Plugin" class - specs.forEach((spec) => { - if (!(spec instanceof api.Plugin)) { - throw new TypeError('unexpected plugin export ' + inspect(spec)); - } - }); - - return specs; - } -} diff --git a/src/legacy/plugin_discovery/plugin_spec/__tests__/is_version_compatible.js b/src/legacy/plugin_discovery/plugin_spec/__tests__/is_version_compatible.js deleted file mode 100644 index 897184496af37..0000000000000 --- a/src/legacy/plugin_discovery/plugin_spec/__tests__/is_version_compatible.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; - -import { isVersionCompatible } from '../is_version_compatible'; - -describe('plugin discovery/plugin spec', () => { - describe('isVersionCompatible()', () => { - const tests = [ - ['kibana', '6.0.0', true], - ['kibana', '6.0.0-rc1', true], - ['6.0.0-rc1', '6.0.0', true], - ['6.0.0', '6.0.0-rc1', true], - ['6.0.0-rc2', '6.0.0-rc1', true], - ['6.0.0-rc2', '6.0.0-rc3', true], - ['foo', 'bar', false], - ['6.0.0', '5.1.4', false], - ['5.1.4', '6.0.0', false], - ['5.1.4-SNAPSHOT', '6.0.0-rc2-SNAPSHOT', false], - ['5.1.4', '6.0.0-rc2-SNAPSHOT', false], - ['5.1.4-SNAPSHOT', '6.0.0', false], - ['5.1.4-SNAPSHOT', '6.0.0-rc2', false], - ]; - - for (const [plugin, kibana, shouldPass] of tests) { - it(`${shouldPass ? 'should' : `shouldn't`} allow plugin: ${plugin} kibana: ${kibana}`, () => { - expect(isVersionCompatible(plugin, kibana)).to.be(shouldPass); - }); - } - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_spec/__tests__/plugin_spec.js b/src/legacy/plugin_discovery/plugin_spec/__tests__/plugin_spec.js deleted file mode 100644 index 02675f0bd60f8..0000000000000 --- a/src/legacy/plugin_discovery/plugin_spec/__tests__/plugin_spec.js +++ /dev/null @@ -1,496 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve } from 'path'; - -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -import { PluginPack } from '../../plugin_pack'; -import { PluginSpec } from '../plugin_spec'; -import * as IsVersionCompatibleNS from '../is_version_compatible'; - -const fooPack = new PluginPack({ - path: '/dev/null', - pkg: { name: 'foo', version: 'kibana' }, -}); - -describe('plugin discovery/plugin spec', () => { - describe('PluginSpec', () => { - describe('validation', () => { - it('throws if missing spec.id AND Pack has no name', () => { - const pack = new PluginPack({ pkg: {} }); - expect(() => new PluginSpec(pack, {})).to.throwError((error) => { - expect(error.message).to.contain('Unable to determine plugin id'); - }); - }); - - it('throws if missing spec.kibanaVersion AND Pack has no version', () => { - const pack = new PluginPack({ pkg: { name: 'foo' } }); - expect(() => new PluginSpec(pack, {})).to.throwError((error) => { - expect(error.message).to.contain('Unable to determine plugin version'); - }); - }); - - it('throws if spec.require is defined, but not an array', () => { - function assert(require) { - expect(() => new PluginSpec(fooPack, { require })).to.throwError((error) => { - expect(error.message).to.contain('"plugin.require" must be an array of plugin ids'); - }); - } - - assert(null); - assert(''); - assert('kibana'); - assert(1); - assert(0); - assert(/a.*b/); - }); - - it('throws if spec.publicDir is truthy and not a string', () => { - function assert(publicDir) { - expect(() => new PluginSpec(fooPack, { publicDir })).to.throwError((error) => { - expect(error.message).to.contain( - `The "path" argument must be of type string. Received type ${typeof publicDir}` - ); - }); - } - - assert(1); - assert(function () {}); - assert([]); - assert(/a.*b/); - }); - - it('throws if spec.publicDir is not an absolute path', () => { - function assert(publicDir) { - expect(() => new PluginSpec(fooPack, { publicDir })).to.throwError((error) => { - expect(error.message).to.contain('plugin.publicDir must be an absolute path'); - }); - } - - assert('relative/path'); - assert('./relative/path'); - }); - - it('throws if spec.publicDir basename is not `public`', () => { - function assert(publicDir) { - expect(() => new PluginSpec(fooPack, { publicDir })).to.throwError((error) => { - expect(error.message).to.contain('must end with a "public" directory'); - }); - } - - assert('/www'); - assert('/www/'); - assert('/www/public/my_plugin'); - assert('/www/public/my_plugin/'); - }); - }); - - describe('#getPack()', () => { - it('returns the pack', () => { - const spec = new PluginSpec(fooPack, {}); - expect(spec.getPack()).to.be(fooPack); - }); - }); - - describe('#getPkg()', () => { - it('returns the pkg from the pack', () => { - const spec = new PluginSpec(fooPack, {}); - expect(spec.getPkg()).to.be(fooPack.getPkg()); - }); - }); - - describe('#getPath()', () => { - it('returns the path from the pack', () => { - const spec = new PluginSpec(fooPack, {}); - expect(spec.getPath()).to.be(fooPack.getPath()); - }); - }); - - describe('#getId()', () => { - it('uses spec.id', () => { - const spec = new PluginSpec(fooPack, { - id: 'bar', - }); - - expect(spec.getId()).to.be('bar'); - }); - - it('defaults to pack.pkg.name', () => { - const spec = new PluginSpec(fooPack, {}); - - expect(spec.getId()).to.be('foo'); - }); - }); - - describe('#getVersion()', () => { - it('uses spec.version', () => { - const spec = new PluginSpec(fooPack, { - version: 'bar', - }); - - expect(spec.getVersion()).to.be('bar'); - }); - - it('defaults to pack.pkg.version', () => { - const spec = new PluginSpec(fooPack, {}); - - expect(spec.getVersion()).to.be('kibana'); - }); - }); - - describe('#isEnabled()', () => { - describe('spec.isEnabled is not defined', () => { - function setup(configPrefix, configGetImpl) { - const spec = new PluginSpec(fooPack, { configPrefix }); - const config = { - get: sinon.spy(configGetImpl), - has: sinon.stub(), - }; - - return { spec, config }; - } - - it('throws if not passed a config service', () => { - const { spec } = setup('a.b.c', () => true); - - expect(() => spec.isEnabled()).to.throwError((error) => { - expect(error.message).to.contain('must be called with a config service'); - }); - expect(() => spec.isEnabled(null)).to.throwError((error) => { - expect(error.message).to.contain('must be called with a config service'); - }); - expect(() => spec.isEnabled({ get: () => {} })).to.throwError((error) => { - expect(error.message).to.contain('must be called with a config service'); - }); - }); - - it('returns true when config.get([...configPrefix, "enabled"]) returns true', () => { - const { spec, config } = setup('d.e.f', () => true); - - expect(spec.isEnabled(config)).to.be(true); - sinon.assert.calledOnce(config.get); - sinon.assert.calledWithExactly(config.get, ['d', 'e', 'f', 'enabled']); - }); - - it('returns false when config.get([...configPrefix, "enabled"]) returns false', () => { - const { spec, config } = setup('g.h.i', () => false); - - expect(spec.isEnabled(config)).to.be(false); - sinon.assert.calledOnce(config.get); - sinon.assert.calledWithExactly(config.get, ['g', 'h', 'i', 'enabled']); - }); - }); - - describe('spec.isEnabled is defined', () => { - function setup(isEnabledImpl) { - const isEnabled = sinon.spy(isEnabledImpl); - const spec = new PluginSpec(fooPack, { isEnabled }); - const config = { - get: sinon.stub(), - has: sinon.stub(), - }; - - return { isEnabled, spec, config }; - } - - it('throws if not passed a config service', () => { - const { spec } = setup(() => true); - - expect(() => spec.isEnabled()).to.throwError((error) => { - expect(error.message).to.contain('must be called with a config service'); - }); - expect(() => spec.isEnabled(null)).to.throwError((error) => { - expect(error.message).to.contain('must be called with a config service'); - }); - expect(() => spec.isEnabled({ get: () => {} })).to.throwError((error) => { - expect(error.message).to.contain('must be called with a config service'); - }); - }); - - it('does not check config if spec.isEnabled returns true', () => { - const { spec, isEnabled, config } = setup(() => true); - - expect(spec.isEnabled(config)).to.be(true); - sinon.assert.calledOnce(isEnabled); - sinon.assert.notCalled(config.get); - }); - - it('does not check config if spec.isEnabled returns false', () => { - const { spec, isEnabled, config } = setup(() => false); - - expect(spec.isEnabled(config)).to.be(false); - sinon.assert.calledOnce(isEnabled); - sinon.assert.notCalled(config.get); - }); - }); - }); - - describe('#getExpectedKibanaVersion()', () => { - describe('has: spec.kibanaVersion,pkg.kibana.version,spec.version,pkg.version', () => { - it('uses spec.kibanaVersion', () => { - const pack = new PluginPack({ - path: '/dev/null', - pkg: { - name: 'expkv', - version: '1.0.0', - kibana: { - version: '6.0.0', - }, - }, - }); - - const spec = new PluginSpec(pack, { - version: '2.0.0', - kibanaVersion: '5.0.0', - }); - - expect(spec.getExpectedKibanaVersion()).to.be('5.0.0'); - }); - }); - describe('missing: spec.kibanaVersion, has: pkg.kibana.version,spec.version,pkg.version', () => { - it('uses pkg.kibana.version', () => { - const pack = new PluginPack({ - path: '/dev/null', - pkg: { - name: 'expkv', - version: '1.0.0', - kibana: { - version: '6.0.0', - }, - }, - }); - - const spec = new PluginSpec(pack, { - version: '2.0.0', - }); - - expect(spec.getExpectedKibanaVersion()).to.be('6.0.0'); - }); - }); - describe('missing: spec.kibanaVersion,pkg.kibana.version, has: spec.version,pkg.version', () => { - it('uses spec.version', () => { - const pack = new PluginPack({ - path: '/dev/null', - pkg: { - name: 'expkv', - version: '1.0.0', - }, - }); - - const spec = new PluginSpec(pack, { - version: '2.0.0', - }); - - expect(spec.getExpectedKibanaVersion()).to.be('2.0.0'); - }); - }); - describe('missing: spec.kibanaVersion,pkg.kibana.version,spec.version, has: pkg.version', () => { - it('uses pkg.version', () => { - const pack = new PluginPack({ - path: '/dev/null', - pkg: { - name: 'expkv', - version: '1.0.0', - }, - }); - - const spec = new PluginSpec(pack, {}); - - expect(spec.getExpectedKibanaVersion()).to.be('1.0.0'); - }); - }); - }); - - describe('#isVersionCompatible()', () => { - it('passes this.getExpectedKibanaVersion() and arg to isVersionCompatible(), returns its result', () => { - const spec = new PluginSpec(fooPack, { version: '1.0.0' }); - sinon.stub(spec, 'getExpectedKibanaVersion').returns('foo'); - const isVersionCompatible = sinon - .stub(IsVersionCompatibleNS, 'isVersionCompatible') - .returns('bar'); - expect(spec.isVersionCompatible('baz')).to.be('bar'); - - sinon.assert.calledOnce(spec.getExpectedKibanaVersion); - sinon.assert.calledWithExactly(spec.getExpectedKibanaVersion); - - sinon.assert.calledOnce(isVersionCompatible); - sinon.assert.calledWithExactly(isVersionCompatible, 'foo', 'baz'); - }); - }); - - describe('#getRequiredPluginIds()', () => { - it('returns spec.require', () => { - const spec = new PluginSpec(fooPack, { require: [1, 2, 3] }); - expect(spec.getRequiredPluginIds()).to.eql([1, 2, 3]); - }); - }); - - describe('#getPublicDir()', () => { - describe('spec.publicDir === false', () => { - it('returns null', () => { - const spec = new PluginSpec(fooPack, { publicDir: false }); - expect(spec.getPublicDir()).to.be(null); - }); - }); - - describe('spec.publicDir is falsy', () => { - it('returns public child of pack path', () => { - function assert(publicDir) { - const spec = new PluginSpec(fooPack, { publicDir }); - expect(spec.getPublicDir()).to.be(resolve('/dev/null/public')); - } - - assert(0); - assert(''); - assert(null); - assert(undefined); - assert(NaN); - }); - }); - - describe('spec.publicDir is an absolute path', () => { - it('returns the path', () => { - const spec = new PluginSpec(fooPack, { - publicDir: '/var/www/public', - }); - - expect(spec.getPublicDir()).to.be('/var/www/public'); - }); - }); - - // NOTE: see constructor tests for other truthy-tests that throw in constructor - }); - - describe('#getExportSpecs()', () => { - it('returns spec.uiExports', () => { - const spec = new PluginSpec(fooPack, { - uiExports: 'foo', - }); - - expect(spec.getExportSpecs()).to.be('foo'); - }); - }); - - describe('#getPreInitHandler()', () => { - it('returns spec.preInit', () => { - const spec = new PluginSpec(fooPack, { - preInit: 'foo', - }); - - expect(spec.getPreInitHandler()).to.be('foo'); - }); - }); - - describe('#getInitHandler()', () => { - it('returns spec.init', () => { - const spec = new PluginSpec(fooPack, { - init: 'foo', - }); - - expect(spec.getInitHandler()).to.be('foo'); - }); - }); - - describe('#getConfigPrefix()', () => { - describe('spec.configPrefix is truthy', () => { - it('returns spec.configPrefix', () => { - const spec = new PluginSpec(fooPack, { - configPrefix: 'foo.bar.baz', - }); - - expect(spec.getConfigPrefix()).to.be('foo.bar.baz'); - }); - }); - describe('spec.configPrefix is falsy', () => { - it('returns spec.getId()', () => { - function assert(configPrefix) { - const spec = new PluginSpec(fooPack, { configPrefix }); - sinon.stub(spec, 'getId').returns('foo'); - expect(spec.getConfigPrefix()).to.be('foo'); - sinon.assert.calledOnce(spec.getId); - } - - assert(false); - assert(null); - assert(undefined); - assert(''); - assert(0); - }); - }); - }); - - describe('#getConfigSchemaProvider()', () => { - it('returns spec.config', () => { - const spec = new PluginSpec(fooPack, { - config: 'foo', - }); - - expect(spec.getConfigSchemaProvider()).to.be('foo'); - }); - }); - - describe('#readConfigValue()', () => { - const spec = new PluginSpec(fooPack, { - configPrefix: 'foo.bar', - }); - - const config = { - get: sinon.stub(), - }; - - afterEach(() => config.get.resetHistory()); - - describe('key = "foo"', () => { - it('passes key as own array item', () => { - spec.readConfigValue(config, 'foo'); - sinon.assert.calledOnce(config.get); - sinon.assert.calledWithExactly(config.get, ['foo', 'bar', 'foo']); - }); - }); - - describe('key = "foo.bar"', () => { - it('passes key as two array items', () => { - spec.readConfigValue(config, 'foo.bar'); - sinon.assert.calledOnce(config.get); - sinon.assert.calledWithExactly(config.get, ['foo', 'bar', 'foo', 'bar']); - }); - }); - - describe('key = ["foo", "bar"]', () => { - it('merged keys into array', () => { - spec.readConfigValue(config, ['foo', 'bar']); - sinon.assert.calledOnce(config.get); - sinon.assert.calledWithExactly(config.get, ['foo', 'bar', 'foo', 'bar']); - }); - }); - }); - - describe('#getDeprecationsProvider()', () => { - it('returns spec.deprecations', () => { - const spec = new PluginSpec(fooPack, { - deprecations: 'foo', - }); - - expect(spec.getDeprecationsProvider()).to.be('foo'); - }); - }); - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_spec/index.js b/src/legacy/plugin_discovery/plugin_spec/index.js deleted file mode 100644 index 671d311b152e2..0000000000000 --- a/src/legacy/plugin_discovery/plugin_spec/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { PluginSpec } from './plugin_spec'; diff --git a/src/legacy/plugin_discovery/plugin_spec/is_version_compatible.js b/src/legacy/plugin_discovery/plugin_spec/is_version_compatible.js deleted file mode 100644 index 6822c168f368d..0000000000000 --- a/src/legacy/plugin_discovery/plugin_spec/is_version_compatible.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { cleanVersion, versionSatisfies } from '../../utils/version'; - -export function isVersionCompatible(version, compatibleWith) { - // the special "kibana" version can be used to always be compatible, - // but is intentionally not supported by the plugin installer - if (version === 'kibana') { - return true; - } - - return versionSatisfies(cleanVersion(version), cleanVersion(compatibleWith)); -} diff --git a/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js b/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js deleted file mode 100644 index db1ec425f2ce5..0000000000000 --- a/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { resolve, basename, isAbsolute as isAbsolutePath } from 'path'; - -import { get, toPath } from 'lodash'; - -import { createInvalidPluginError } from '../errors'; -import { isVersionCompatible } from './is_version_compatible'; - -export class PluginSpec { - /** - * @param {PluginPack} pack The plugin pack that produced this spec - * @param {Object} opts the options for this plugin - * @param {String} [opts.id=pkg.name] the id for this plugin. - * @param {Object} [opts.uiExports] a mapping of UiExport types to - * UI modules or metadata about the UI module - * @param {Array} [opts.require] the other plugins that this plugin - * requires. These plugins must exist and be enabled for this plugin - * to function. The require'd plugins will also be initialized first, - * in order to make sure that dependencies provided by these plugins - * are available - * @param {String} [opts.version=pkg.version] the version of this plugin - * @param {Function} [opts.init] A function that will be called to initialize - * this plugin at the appropriate time. - * @param {Function} [opts.configPrefix=this.id] The prefix to use for - * configuration values in the main configuration service - * @param {Function} [opts.config] A function that produces a configuration - * schema using Joi, which is passed as its first argument. - * @param {String|False} [opts.publicDir=path + '/public'] the public - * directory for this plugin. The final directory must have the name "public", - * though it can be located somewhere besides the root of the plugin. Set - * this to false to disable exposure of a public directory - */ - constructor(pack, options) { - const { - id, - require, - version, - kibanaVersion, - uiExports, - uiCapabilities, - publicDir, - configPrefix, - config, - deprecations, - preInit, - init, - postInit, - isEnabled, - } = options; - - this._id = id; - this._pack = pack; - this._version = version; - this._kibanaVersion = kibanaVersion; - this._require = require; - - this._publicDir = publicDir; - this._uiExports = uiExports; - this._uiCapabilities = uiCapabilities; - - this._configPrefix = configPrefix; - this._configSchemaProvider = config; - this._configDeprecationsProvider = deprecations; - - this._isEnabled = isEnabled; - this._preInit = preInit; - this._init = init; - this._postInit = postInit; - - if (!this.getId()) { - throw createInvalidPluginError(this, 'Unable to determine plugin id'); - } - - if (!this.getVersion()) { - throw createInvalidPluginError(this, 'Unable to determine plugin version'); - } - - if (this.getRequiredPluginIds() !== undefined && !Array.isArray(this.getRequiredPluginIds())) { - throw createInvalidPluginError(this, '"plugin.require" must be an array of plugin ids'); - } - - if (this._publicDir) { - if (!isAbsolutePath(this._publicDir)) { - throw createInvalidPluginError(this, 'plugin.publicDir must be an absolute path'); - } - if (basename(this._publicDir) !== 'public') { - throw createInvalidPluginError( - this, - `publicDir for plugin ${this.getId()} must end with a "public" directory.` - ); - } - } - } - - getPack() { - return this._pack; - } - - getPkg() { - return this._pack.getPkg(); - } - - getPath() { - return this._pack.getPath(); - } - - getId() { - return this._id || this.getPkg().name; - } - - getVersion() { - return this._version || this.getPkg().version; - } - - isEnabled(config) { - if (!config || typeof config.get !== 'function' || typeof config.has !== 'function') { - throw new TypeError('PluginSpec#isEnabled() must be called with a config service'); - } - - if (this._isEnabled) { - return this._isEnabled(config); - } - - return Boolean(this.readConfigValue(config, 'enabled')); - } - - getExpectedKibanaVersion() { - // Plugins must specify their version, and by default that version should match - // the version of kibana down to the patch level. If these two versions need - // to diverge, they can specify a kibana.version in the package to indicate the - // version of kibana the plugin is intended to work with. - return ( - this._kibanaVersion || get(this.getPack().getPkg(), 'kibana.version') || this.getVersion() - ); - } - - isVersionCompatible(actualKibanaVersion) { - return isVersionCompatible(this.getExpectedKibanaVersion(), actualKibanaVersion); - } - - getRequiredPluginIds() { - return this._require; - } - - getPublicDir() { - if (this._publicDir === false) { - return null; - } - - if (!this._publicDir) { - return resolve(this.getPack().getPath(), 'public'); - } - - return this._publicDir; - } - - getExportSpecs() { - return this._uiExports; - } - - getUiCapabilitiesProvider() { - return this._uiCapabilities; - } - - getPreInitHandler() { - return this._preInit; - } - - getInitHandler() { - return this._init; - } - - getPostInitHandler() { - return this._postInit; - } - - getConfigPrefix() { - return this._configPrefix || this.getId(); - } - - getConfigSchemaProvider() { - return this._configSchemaProvider; - } - - readConfigValue(config, key) { - return config.get([...toPath(this.getConfigPrefix()), ...toPath(key)]); - } - - getDeprecationsProvider() { - return this._configDeprecationsProvider; - } -} diff --git a/src/legacy/plugin_discovery/plugin_spec/plugin_spec_options.d.ts b/src/legacy/plugin_discovery/plugin_spec/plugin_spec_options.d.ts deleted file mode 100644 index e1ed2f57375a4..0000000000000 --- a/src/legacy/plugin_discovery/plugin_spec/plugin_spec_options.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { Server } from '../../server/kbn_server'; -import { Capabilities } from '../../../core/server'; - -export type InitPluginFunction = (server: Server) => void; -export interface UiExports { - injectDefaultVars?: (server: Server) => { [key: string]: any }; -} - -export interface PluginSpecOptions { - id: string; - require?: string[]; - publicDir?: string; - uiExports?: UiExports; - uiCapabilities?: Capabilities; - init?: InitPluginFunction; - config?: any; -} diff --git a/src/legacy/plugin_discovery/types.ts b/src/legacy/plugin_discovery/types.ts deleted file mode 100644 index 700ca6fa68c95..0000000000000 --- a/src/legacy/plugin_discovery/types.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Server } from '../server/kbn_server'; -import { Capabilities } from '../../core/server'; -import { AppCategory } from '../../core/types'; - -/** - * Usage - * - * ``` - * const apmOss: LegacyPlugin = (kibana) => { - * return new kibana.Plugin({ - * id: 'apm_oss', - * // ... - * }); - * }; - * ``` - */ -export type LegacyPluginInitializer = (kibana: LegacyPluginApi) => ArrayOrItem; - -export type ArrayOrItem = T | T[]; - -export interface LegacyPluginApi { - Plugin: new (options: Partial) => LegacyPluginSpec; -} - -export interface LegacyPluginOptions { - id: string; - require: string[]; - version: string; - kibanaVersion: 'kibana'; - uiExports: Partial<{ - app: Partial<{ - title: string; - category?: AppCategory; - description: string; - main: string; - icon: string; - euiIconType: string; - order: number; - listed: boolean; - }>; - apps: any; - hacks: string[]; - visualize: string[]; - devTools: string[]; - injectDefaultVars: (server: Server) => Record; - home: string[]; - mappings: any; - migrations: any; - visTypes: string[]; - embeddableActions?: string[]; - embeddableFactories?: string[]; - uiSettingDefaults?: Record; - interpreter: string | string[]; - }>; - uiCapabilities?: Capabilities; - publicDir: any; - configPrefix: any; - config: any; - deprecations: any; - preInit: any; - init: InitPluginFunction; - postInit: any; - isEnabled: boolean; -} - -export type InitPluginFunction = (server: Server) => void; - -export interface LegacyPluginSpec { - getPack(): any; - getPkg(): any; - getPath(): string; - getId(): string; - getVersion(): string; - isEnabled(config: any): boolean; - getExpectedKibanaVersion(): string; - isVersionCompatible(actualKibanaVersion: any): boolean; - getRequiredPluginIds(): string[]; - getPublicDir(): string | null; - getExportSpecs(): any; - getUiCapabilitiesProvider(): any; - getPreInitHandler(): any; - getInitHandler(): any; - getPostInitHandler(): any; - getConfigPrefix(): string; - getConfigSchemaProvider(): any; - readConfigValue(config: any, key: string): any; - getDeprecationsProvider(): any; -} diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index f8736fb30f90e..a94766ef06926 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -131,6 +131,7 @@ export default () => }), }).default(), + // still used by the legacy i18n mixin plugins: Joi.object({ paths: Joi.array().items(Joi.string()).default([]), scanDirs: Joi.array().items(Joi.string()).default([]), @@ -146,71 +147,8 @@ export default () => status: Joi.object({ allowAnonymous: Joi.boolean().default(false), }).default(), - map: Joi.object({ - includeElasticMapsService: Joi.boolean().default(true), - proxyElasticMapsServiceInMaps: Joi.boolean().default(false), - tilemap: Joi.object({ - url: Joi.string(), - options: Joi.object({ - attribution: Joi.string(), - minZoom: Joi.number().min(0, 'Must be 0 or higher').default(0), - maxZoom: Joi.number().default(10), - tileSize: Joi.number(), - subdomains: Joi.array().items(Joi.string()).single(), - errorTileUrl: Joi.string().uri(), - tms: Joi.boolean(), - reuseTiles: Joi.boolean(), - bounds: Joi.array().items(Joi.array().items(Joi.number()).min(2).required()).min(2), - default: Joi.boolean(), - }).default({ - default: true, - }), - }).default(), - regionmap: Joi.object({ - includeElasticMapsService: Joi.boolean().default(true), - layers: Joi.array() - .items( - Joi.object({ - url: Joi.string(), - format: Joi.object({ - type: Joi.string().default('geojson'), - }).default({ - type: 'geojson', - }), - meta: Joi.object({ - feature_collection_path: Joi.string().default('data'), - }).default({ - feature_collection_path: 'data', - }), - attribution: Joi.string(), - name: Joi.string(), - fields: Joi.array().items( - Joi.object({ - name: Joi.string(), - description: Joi.string(), - }) - ), - }) - ) - .default([]), - }).default(), - manifestServiceUrl: Joi.string().default('').allow(''), - emsFileApiUrl: Joi.string().default('https://vector.maps.elastic.co'), - emsTileApiUrl: Joi.string().default('https://tiles.maps.elastic.co'), - emsLandingPageUrl: Joi.string().default('https://maps.elastic.co/v7.9'), - emsFontLibraryUrl: Joi.string().default( - 'https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf' - ), - emsTileLayerId: Joi.object({ - bright: Joi.string().default('road_map'), - desaturated: Joi.string().default('road_map_desaturated'), - dark: Joi.string().default('dark_map'), - }).default({ - bright: 'road_map', - desaturated: 'road_map_desaturated', - dark: 'dark_map', - }), - }).default(), + + map: HANDLED_IN_NEW_PLATFORM, i18n: Joi.object({ locale: Joi.string().default('en'), diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 3cfda0e0696bb..1718a9a8f55da 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -26,11 +26,10 @@ import { LoggerFactory, PackageInfo, LegacyServiceSetupDeps, - LegacyServiceDiscoverPlugins, } from '../../core/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LegacyConfig, ILegacyInternals } from '../../core/server/legacy'; +import { LegacyConfig } from '../../core/server/legacy'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { UiPlugins } from '../../core/server/plugins'; @@ -58,9 +57,7 @@ export interface PluginsSetup { export interface KibanaCore { __internals: { - elasticsearch: LegacyServiceSetupDeps['core']['elasticsearch']; hapiServer: LegacyServiceSetupDeps['core']['http']['server']; - legacy: ILegacyInternals; rendering: LegacyServiceSetupDeps['core']['rendering']; uiPlugins: UiPlugins; }; @@ -90,31 +87,18 @@ export interface NewPlatform { stop: null; } -export type LegacyPlugins = Pick< - LegacyServiceDiscoverPlugins, - 'pluginSpecs' | 'disabledPluginSpecs' | 'uiExports' ->; - // eslint-disable-next-line import/no-default-export export default class KbnServer { public readonly newPlatform: NewPlatform; public server: Server; public inject: Server['inject']; - public pluginSpecs: any[]; - public uiBundles: any; - constructor( - settings: Record, - config: KibanaConfig, - core: KibanaCore, - legacyPlugins: LegacyPlugins - ); + constructor(settings: Record, config: KibanaConfig, core: KibanaCore); public ready(): Promise; public mixin(...fns: KbnMixinFunc[]): Promise; public listen(): Promise; public close(): Promise; - public afterPluginsInit(callback: () => void): void; public applyLoggingConfiguration(settings: any): void; public config: KibanaConfig; } diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 107e5f6387833..e29563a7c6266 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -30,7 +30,6 @@ import { loggingMixin } from './logging'; import warningsMixin from './warnings'; import configCompleteMixin from './config/complete'; import { optimizeMixin } from '../../optimize'; -import * as Plugins from './plugins'; import { uiMixin } from '../ui'; import { i18nMixin } from './i18n'; @@ -47,9 +46,8 @@ export default class KbnServer { * @param {Record} settings * @param {KibanaConfig} config * @param {KibanaCore} core - * @param {LegacyPlugins} legacyPlugins */ - constructor(settings, config, core, legacyPlugins) { + constructor(settings, config, core) { this.name = pkg.name; this.version = pkg.version; this.build = pkg.build || false; @@ -74,14 +72,8 @@ export default class KbnServer { stop: null, }; - this.uiExports = legacyPlugins.uiExports; - this.pluginSpecs = legacyPlugins.pluginSpecs; - this.disabledPluginSpecs = legacyPlugins.disabledPluginSpecs; - this.ready = constant( this.mixin( - Plugins.waitForInitSetupMixin, - // Sets global HTTP behaviors httpMixin, @@ -93,22 +85,13 @@ export default class KbnServer { // scan translations dirs, register locale files and initialize i18n engine. i18nMixin, - // find plugins and set this.plugins and this.pluginSpecs - Plugins.scanMixin, - // tell the config we are done loading plugins configCompleteMixin, uiMixin, // setup routes that serve the @kbn/optimizer output - optimizeMixin, - - // initialize the plugins - Plugins.initializeMixin, - - // notify any deferred setup logic that plugins have initialized - Plugins.waitForInitResolveMixin + optimizeMixin ) ); diff --git a/src/legacy/server/plugins/index.js b/src/legacy/server/plugins/index.js deleted file mode 100644 index 1511b63b519ae..0000000000000 --- a/src/legacy/server/plugins/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { scanMixin } from './scan_mixin'; -export { initializeMixin } from './initialize_mixin'; -export { waitForInitSetupMixin, waitForInitResolveMixin } from './wait_for_plugins_init'; diff --git a/src/legacy/server/plugins/initialize_mixin.js b/src/legacy/server/plugins/initialize_mixin.js deleted file mode 100644 index ccf4cd1c1a404..0000000000000 --- a/src/legacy/server/plugins/initialize_mixin.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { callPluginHook } from './lib'; - -/** - * KbnServer mixin that initializes all plugins found in ./scan mixin - * @param {KbnServer} kbnServer - * @param {Hapi.Server} server - * @param {Config} config - * @return {Promise} - */ -export async function initializeMixin(kbnServer, server, config) { - if (!config.get('plugins.initialize')) { - server.log(['info'], 'Plugin initialization disabled.'); - return; - } - - async function callHookOnPlugins(hookName) { - const { plugins } = kbnServer; - const ids = plugins.map((p) => p.id); - - for (const id of ids) { - await callPluginHook(hookName, plugins, id, []); - } - } - - await callHookOnPlugins('preInit'); - await callHookOnPlugins('init'); - await callHookOnPlugins('postInit'); -} diff --git a/src/legacy/server/plugins/lib/call_plugin_hook.js b/src/legacy/server/plugins/lib/call_plugin_hook.js deleted file mode 100644 index b665869f5d25f..0000000000000 --- a/src/legacy/server/plugins/lib/call_plugin_hook.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { last } from 'lodash'; - -export async function callPluginHook(hookName, plugins, id, history) { - const plugin = plugins.find((plugin) => plugin.id === id); - - // make sure this is a valid plugin id - if (!plugin) { - if (history.length) { - throw new Error(`Unmet requirement "${id}" for plugin "${last(history)}"`); - } else { - throw new Error(`Unknown plugin "${id}"`); - } - } - - const circleStart = history.indexOf(id); - const path = [...history, id]; - - // make sure we are not trying to load a dependency within itself - if (circleStart > -1) { - const circle = path.slice(circleStart); - throw new Error(`circular dependency found: "${circle.join(' -> ')}"`); - } - - // call hook on all dependencies - for (const req of plugin.requiredIds) { - await callPluginHook(hookName, plugins, req, path); - } - - // call hook on this plugin - await plugin[hookName](); -} diff --git a/src/legacy/server/plugins/lib/call_plugin_hook.test.js b/src/legacy/server/plugins/lib/call_plugin_hook.test.js deleted file mode 100644 index 30dc2d91a9ab2..0000000000000 --- a/src/legacy/server/plugins/lib/call_plugin_hook.test.js +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import sinon from 'sinon'; -import { callPluginHook } from './call_plugin_hook'; - -describe('server/plugins/callPluginHook', () => { - it('should call in correct order based on requirements', async () => { - const plugins = [ - { - id: 'foo', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: ['bar', 'baz'], - }, - { - id: 'bar', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: [], - }, - { - id: 'baz', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: ['bar'], - }, - ]; - - await callPluginHook('init', plugins, 'foo', []); - const [foo, bar, baz] = plugins; - sinon.assert.calledOnce(foo.init); - sinon.assert.calledTwice(bar.init); - sinon.assert.calledOnce(baz.init); - sinon.assert.callOrder(bar.init, baz.init, foo.init); - }); - - it('throws meaningful error when required plugin is missing', async () => { - const plugins = [ - { - id: 'foo', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: ['bar'], - }, - ]; - - try { - await callPluginHook('init', plugins, 'foo', []); - throw new Error('expected callPluginHook to throw'); - } catch (error) { - expect(error.message).toContain('"bar" for plugin "foo"'); - } - }); - - it('throws meaningful error when dependencies are circular', async () => { - const plugins = [ - { - id: 'foo', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: ['bar'], - }, - { - id: 'bar', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: ['baz'], - }, - { - id: 'baz', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: ['foo'], - }, - ]; - - try { - await callPluginHook('init', plugins, 'foo', []); - throw new Error('expected callPluginHook to throw'); - } catch (error) { - expect(error.message).toContain('foo -> bar -> baz -> foo'); - } - }); -}); diff --git a/src/legacy/server/plugins/lib/index.js b/src/legacy/server/plugins/lib/index.js deleted file mode 100644 index 2329d24498b6b..0000000000000 --- a/src/legacy/server/plugins/lib/index.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { callPluginHook } from './call_plugin_hook'; -export { Plugin } from './plugin'; diff --git a/src/legacy/server/plugins/lib/plugin.js b/src/legacy/server/plugins/lib/plugin.js deleted file mode 100644 index 48389061199ff..0000000000000 --- a/src/legacy/server/plugins/lib/plugin.js +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { once } from 'lodash'; - -/** - * The server plugin class, used to extend the server - * and add custom behavior. A "scoped" plugin class is - * created by the PluginApi class and provided to plugin - * providers that automatically binds all but the `opts` - * arguments. - * - * @class Plugin - * @param {KbnServer} kbnServer - the KbnServer this plugin - * belongs to. - * @param {PluginDefinition} def - * @param {PluginSpec} spec - */ -export class Plugin { - constructor(kbnServer, spec) { - this.kbnServer = kbnServer; - this.spec = spec; - this.pkg = spec.getPkg(); - this.path = spec.getPath(); - this.id = spec.getId(); - this.version = spec.getVersion(); - this.requiredIds = spec.getRequiredPluginIds() || []; - this.externalPreInit = spec.getPreInitHandler(); - this.externalInit = spec.getInitHandler(); - this.externalPostInit = spec.getPostInitHandler(); - this.enabled = spec.isEnabled(kbnServer.config); - this.configPrefix = spec.getConfigPrefix(); - this.publicDir = spec.getPublicDir(); - - this.preInit = once(this.preInit); - this.init = once(this.init); - this.postInit = once(this.postInit); - } - - async preInit() { - if (this.externalPreInit) { - return await this.externalPreInit(this.kbnServer.server); - } - } - - async init() { - const { id, version, kbnServer, configPrefix } = this; - const { config } = kbnServer; - - // setup the hapi register function and get on with it - const register = async (server, options) => { - this._server = server; - this._options = options; - - server.logWithMetadata(['plugins', 'debug'], `Initializing plugin ${this.toString()}`, { - plugin: this, - }); - - if (this.publicDir) { - server.newPlatform.__internals.http.registerStaticDir( - `/plugins/${id}/{path*}`, - this.publicDir - ); - } - - if (this.externalInit) { - await this.externalInit(server, options); - } - }; - - await kbnServer.server.register({ - plugin: { register, name: id, version }, - options: config.has(configPrefix) ? config.get(configPrefix) : null, - }); - } - - async postInit() { - if (this.externalPostInit) { - return await this.externalPostInit(this.kbnServer.server); - } - } - - getServer() { - return this._server; - } - - getOptions() { - return this._options; - } - - toJSON() { - return this.pkg; - } - - toString() { - return `${this.id}@${this.version}`; - } -} diff --git a/src/legacy/server/plugins/scan_mixin.js b/src/legacy/server/plugins/scan_mixin.js deleted file mode 100644 index 89ebaf920d9d1..0000000000000 --- a/src/legacy/server/plugins/scan_mixin.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { Plugin } from './lib'; - -export async function scanMixin(kbnServer) { - kbnServer.plugins = kbnServer.pluginSpecs.map((spec) => new Plugin(kbnServer, spec)); -} diff --git a/src/legacy/server/plugins/wait_for_plugins_init.js b/src/legacy/server/plugins/wait_for_plugins_init.js deleted file mode 100644 index 144eb5ef803cc..0000000000000 --- a/src/legacy/server/plugins/wait_for_plugins_init.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Tracks the individual queue for each kbnServer, rather than attaching - * it to the kbnServer object via a property or something - * @type {WeakMap} - */ -const queues = new WeakMap(); - -export function waitForInitSetupMixin(kbnServer) { - queues.set(kbnServer, []); - - kbnServer.afterPluginsInit = function (callback) { - const queue = queues.get(kbnServer); - - if (!queue) { - throw new Error( - 'Plugins have already initialized. Only use this method for setup logic that must wait for plugins to initialize.' - ); - } - - queue.push(callback); - }; -} - -export async function waitForInitResolveMixin(kbnServer, server, config) { - const queue = queues.get(kbnServer); - queues.set(kbnServer, null); - - // only actually call the callbacks if we are really initializing - if (config.get('plugins.initialize')) { - for (const cb of queue) { - await cb(); - } - } -} diff --git a/src/legacy/types.ts b/src/legacy/types.ts deleted file mode 100644 index 43c9ac79538b1..0000000000000 --- a/src/legacy/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export * from './plugin_discovery/types'; diff --git a/src/legacy/ui/__tests__/fixtures/plugin_async_foo/index.js b/src/legacy/ui/__tests__/fixtures/plugin_async_foo/index.js deleted file mode 100644 index afe618c6d3d9c..0000000000000 --- a/src/legacy/ui/__tests__/fixtures/plugin_async_foo/index.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Bluebird from 'bluebird'; - -export default (kibana) => - new kibana.Plugin({ - config(Joi) { - return Joi.object() - .keys({ - enabled: Joi.boolean().default(true), - delay: Joi.number().required(), - shared: Joi.string(), - }) - .default(); - }, - - uiExports: { - async injectDefaultVars(server, options) { - await Bluebird.delay(options.delay); - return { shared: options.shared }; - }, - }, - }); diff --git a/src/legacy/ui/__tests__/fixtures/plugin_async_foo/package.json b/src/legacy/ui/__tests__/fixtures/plugin_async_foo/package.json deleted file mode 100644 index fc1c8d8088f1b..0000000000000 --- a/src/legacy/ui/__tests__/fixtures/plugin_async_foo/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "plugin_async_foo", - "version": "kibana" -} diff --git a/src/legacy/ui/__tests__/fixtures/plugin_bar/index.js b/src/legacy/ui/__tests__/fixtures/plugin_bar/index.js deleted file mode 100644 index 975a1dc7c92e7..0000000000000 --- a/src/legacy/ui/__tests__/fixtures/plugin_bar/index.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export default (kibana) => - new kibana.Plugin({ - config(Joi) { - return Joi.object() - .keys({ - enabled: Joi.boolean().default(true), - shared: Joi.string(), - }) - .default(); - }, - - uiExports: { - injectDefaultVars(server, options) { - return { shared: options.shared }; - }, - }, - }); diff --git a/src/legacy/ui/__tests__/fixtures/plugin_bar/package.json b/src/legacy/ui/__tests__/fixtures/plugin_bar/package.json deleted file mode 100644 index f79b807990dca..0000000000000 --- a/src/legacy/ui/__tests__/fixtures/plugin_bar/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "plugin_bar", - "version": "kibana" -} diff --git a/src/legacy/ui/__tests__/fixtures/plugin_foo/index.js b/src/legacy/ui/__tests__/fixtures/plugin_foo/index.js deleted file mode 100644 index 975a1dc7c92e7..0000000000000 --- a/src/legacy/ui/__tests__/fixtures/plugin_foo/index.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export default (kibana) => - new kibana.Plugin({ - config(Joi) { - return Joi.object() - .keys({ - enabled: Joi.boolean().default(true), - shared: Joi.string(), - }) - .default(); - }, - - uiExports: { - injectDefaultVars(server, options) { - return { shared: options.shared }; - }, - }, - }); diff --git a/src/legacy/ui/__tests__/fixtures/plugin_foo/package.json b/src/legacy/ui/__tests__/fixtures/plugin_foo/package.json deleted file mode 100644 index c1b7ddd35c9a2..0000000000000 --- a/src/legacy/ui/__tests__/fixtures/plugin_foo/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "plugin_foo", - "version": "kibana" -} diff --git a/src/legacy/ui/__tests__/fixtures/test_app/index.js b/src/legacy/ui/__tests__/fixtures/test_app/index.js deleted file mode 100644 index 3eddefd618ce0..0000000000000 --- a/src/legacy/ui/__tests__/fixtures/test_app/index.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export default (kibana) => - new kibana.Plugin({ - uiExports: { - app: { - name: 'test_app', - main: 'plugins/test_app/index.js', - }, - - injectDefaultVars() { - return { - from_defaults: true, - }; - }, - }, - init(server) { - server.injectUiAppVars('test_app', () => ({ - from_test_app: true, - })); - }, - }); diff --git a/src/legacy/ui/__tests__/fixtures/test_app/package.json b/src/legacy/ui/__tests__/fixtures/test_app/package.json deleted file mode 100644 index 3aeb029e4f4cc..0000000000000 --- a/src/legacy/ui/__tests__/fixtures/test_app/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "test_app", - "version": "kibana" -} diff --git a/src/legacy/ui/index.js b/src/legacy/ui/index.js index 05373fa5d1964..5c06cb4677347 100644 --- a/src/legacy/ui/index.js +++ b/src/legacy/ui/index.js @@ -18,4 +18,3 @@ */ export { uiMixin } from './ui_mixin'; -export { collectUiExports } from './ui_exports'; diff --git a/src/legacy/ui/ui_exports/README.md b/src/legacy/ui/ui_exports/README.md deleted file mode 100644 index 7fb117b1c25b9..0000000000000 --- a/src/legacy/ui/ui_exports/README.md +++ /dev/null @@ -1,95 +0,0 @@ -# UI Exports - -When defining a Plugin, the `uiExports` key can be used to define a map of export types to values that will be used to configure the UI system. A common use for `uiExports` is `uiExports.app`, which defines the configuration of a [`UiApp`][UiApp] and teaches the UI System how to render, bundle and tell the user about an application. - - -## `collectUiExports(pluginSpecs): { [type: string]: any }` - -This function produces the object commonly found at `kbnServer.uiExports`. This object is created by calling `collectPluginExports()` with a standard set of export type reducers and defaults for the UI System. - -### export type reducers - -The [`ui_export_types` module][UiExportTypes] defines the reducer used for each uiExports key (or `type`). The name of every export in [./ui_export_types/index.js][UiExportTypes] is a key that plugins can define in their `uiExports` specification and the value of those exports are reducers that `collectPluginExports()` will call to produce the merged result of all export specs. - -### example - UiApps - -Plugin authors can define a new UiApp in their plugin specification like so: - -```js -// a single app export -export default function (kibana) { - return new kibana.Plugin({ - //... - uiExports: { - app: { - // uiApp spec options go here - } - } - }) -} - -// apps can also export multiple apps -export default function (kibana) { - return new kibana.Plugin({ - //... - uiExports: { - apps: [ - { /* uiApp spec options */ }, - { /* second uiApp spec options */ }, - ] - } - }) -} -``` - -To handle this export type, the [ui_export_types][UiExportTypes] module exports two reducers, one named `app` and the other `apps`. - -```js -export const app = ... -export const apps = ... -``` - -These reducers are defined in [`ui_export_types/ui_apps`][UiAppExportType] and have the exact same definition: - -```js -// `wrap()` produces a reducer by wrapping a base reducer with modifiers. -// All but the last argument are modifiers that take a reducer and return -// an alternate reducer to use in it's place. -// -// Most wrappers call their target reducer with slightly different -// arguments. This allows composing standard reducer modifications for -// reuse, consistency, and easy reference (once you get the hang of it). -wrap( - // calls the next reducer with the `type` set to `uiAppSpecs`, ignoring - // the key the plugin author used to define this spec ("app" or "apps" - // in this example) - alias('uiAppSpecs'), - - // calls the next reducer with the `spec` set to the result of calling - // `applySpecDefaults(spec, type, pluginSpec)` which merges some defaults - // from the `PluginSpec` because we want uiAppSpecs to be useful individually - mapSpec(applySpecDefaults), - - // writes this spec to `acc[type]` (`acc.uiAppSpecs` in this example since - // the type was set to `uiAppSpecs` by `alias()`). It does this by concatenating - // the current value and the spec into an array. If either item is already - // an array its items are added to the result individually. If either item - // is undefined it is ignored. - // - // NOTE: since flatConcatAtType is last it isn't a wrapper, it's - // just a normal reducer - flatConcatAtType -) -``` - -This reducer format was chosen so that it will be easier to look back at these reducers and see that `app` and `apps` export specs are written to `kbnServer.uiExports.uiAppSpecs`, with defaults applied, in an array. - -### defaults - -The [`ui_exports/ui_export_defaults`][UiExportDefaults] module defines the default shape of the uiExports object produced by `collectUiExports()`. The defaults generally describe the `uiExports` from the UI System itself, like default visTypes and such. - -[UiExportDefaults]: ./ui_export_defaults.js "uiExport defaults definition" -[UiExportTypes]: ./ui_export_types/index.js "Index of default ui_export_types module" -[UiAppExportType]: ./ui_export_types/ui_apps.js "UiApp extension type definition" -[PluginSpec]: ../../plugin_discovery/plugin_spec/plugin_spec.js "PluginSpec class definition" -[PluginDiscovery]: '../../plugin_discovery' "plugin_discovery module" \ No newline at end of file diff --git a/src/legacy/ui/ui_exports/collect_ui_exports.ts b/src/legacy/ui/ui_exports/collect_ui_exports.ts deleted file mode 100644 index edb2a11dc0527..0000000000000 --- a/src/legacy/ui/ui_exports/collect_ui_exports.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { LegacyUiExports } from '../../../core/server'; - -// @ts-ignore -import { UI_EXPORT_DEFAULTS } from './ui_export_defaults'; -// @ts-ignore -import * as uiExportTypeReducers from './ui_export_types'; -// @ts-ignore -import { reduceExportSpecs } from '../../plugin_discovery'; - -export function collectUiExports(pluginSpecs: unknown[]): LegacyUiExports { - return reduceExportSpecs(pluginSpecs, uiExportTypeReducers, UI_EXPORT_DEFAULTS); -} diff --git a/src/legacy/ui/ui_exports/index.js b/src/legacy/ui/ui_exports/index.js deleted file mode 100644 index 56db698dc7b03..0000000000000 --- a/src/legacy/ui/ui_exports/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { collectUiExports } from './collect_ui_exports'; diff --git a/src/legacy/ui/ui_exports/ui_export_defaults.js b/src/legacy/ui/ui_exports/ui_export_defaults.js deleted file mode 100644 index 227954155ce88..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_defaults.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export const UI_EXPORT_DEFAULTS = {}; diff --git a/src/legacy/ui/ui_exports/ui_export_types/index.js b/src/legacy/ui/ui_exports/ui_export_types/index.js deleted file mode 100644 index 9ff6a53f4afb9..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/index.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { injectDefaultVars, replaceInjectedVars } from './modify_injected_vars'; - -export { - mappings, - migrations, - savedObjectSchemas, - savedObjectsManagement, - validations, -} from './saved_object'; - -export { taskDefinitions } from './task_definitions'; - -export { link, links } from './ui_nav_links'; - -export { uiSettingDefaults } from './ui_settings'; - -export { unknown } from './unknown'; diff --git a/src/legacy/ui/ui_exports/ui_export_types/modify_injected_vars.js b/src/legacy/ui/ui_exports/ui_export_types/modify_injected_vars.js deleted file mode 100644 index 4bb9f350bd959..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/modify_injected_vars.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { flatConcatAtType } from './reduce'; -import { wrap, alias, mapSpec } from './modify_reduce'; - -export const replaceInjectedVars = wrap(alias('injectedVarsReplacers'), flatConcatAtType); - -export const injectDefaultVars = wrap( - alias('defaultInjectedVarProviders'), - mapSpec((spec, type, pluginSpec) => ({ - pluginSpec, - fn: spec, - })), - flatConcatAtType -); diff --git a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/alias.js b/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/alias.js deleted file mode 100644 index a894e59a03c81..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/alias.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Creates a reducer wrapper which, when called with a reducer, creates a new - * reducer that replaces the `type` value with `newType` before delegating to - * the wrapped reducer - * @param {String} newType - * @return {Function} - */ -export const alias = (newType) => (next) => (acc, spec, type, pluginSpec) => - next(acc, spec, newType, pluginSpec); diff --git a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/debug.js b/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/debug.js deleted file mode 100644 index c40bca59fe14c..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/debug.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { mapSpec } from './map_spec'; - -/** - * Reducer wrapper which, replaces the `spec` with the details about the definition - * of that spec - * @type {Function} - */ -export const debug = mapSpec((spec, type, pluginSpec) => ({ - spec, - type, - pluginSpec, -})); diff --git a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/index.js b/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/index.js deleted file mode 100644 index 54c81fefdd08a..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/index.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { alias } from './alias'; -export { debug } from './debug'; -export { mapSpec } from './map_spec'; -export { wrap } from './wrap'; -export { uniqueKeys } from './unique_keys'; diff --git a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/map_spec.js b/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/map_spec.js deleted file mode 100644 index 5970c45e7445e..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/map_spec.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Creates a reducer wrapper which, when called with a reducer, creates a new - * reducer that replaces the `specs` value with the result of calling - * `mapFn(spec, type, pluginSpec)` before delegating to the wrapped - * reducer - * @param {Function} mapFn receives `(specs, type, pluginSpec)` - * @return {Function} - */ -export const mapSpec = (mapFn) => (next) => (acc, spec, type, pluginSpec) => - next(acc, mapFn(spec, type, pluginSpec), type, pluginSpec); diff --git a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/unique_keys.js b/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/unique_keys.js deleted file mode 100644 index dedcd057b09e3..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/unique_keys.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const pluginId = (pluginSpec) => (pluginSpec.id ? pluginSpec.id() : pluginSpec.getId()); - -export const uniqueKeys = (sourceType) => (next) => (acc, spec, type, pluginSpec) => { - const duplicates = Object.keys(spec).filter((key) => acc[type] && acc[type].hasOwnProperty(key)); - - if (duplicates.length) { - throw new Error( - `${pluginId(pluginSpec)} defined duplicate ${sourceType || type} values: ${duplicates}` - ); - } - - return next(acc, spec, type, pluginSpec); -}; diff --git a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/wrap.js b/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/wrap.js deleted file mode 100644 index f84d83ed7c845..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/wrap.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Wrap a function with any number of wrappers. Wrappers - * are functions that take a reducer and return a reducer - * that should be called in its place. The wrappers will - * be called in reverse order for setup and then in the - * order they are defined when the resulting reducer is - * executed. - * - * const reducer = wrap( - * next => (acc) => acc[1] = 'a', - * next => (acc) => acc[1] = 'b', - * next => (acc) => acc[1] = 'c' - * ) - * - * reducer('foo') //=> 'fco' - * - * @param {Function} ...wrappers - * @param {Function} reducer - * @return {Function} - */ -export function wrap(...args) { - const reducer = args[args.length - 1]; - const wrappers = args.slice(0, -1); - - return wrappers.reverse().reduce((acc, wrapper) => wrapper(acc), reducer); -} diff --git a/src/legacy/ui/ui_exports/ui_export_types/reduce/flat_concat_at_type.js b/src/legacy/ui/ui_exports/ui_export_types/reduce/flat_concat_at_type.js deleted file mode 100644 index 5fcbcac463392..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/reduce/flat_concat_at_type.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { createTypeReducer, flatConcat } from './lib'; - -/** - * Reducer that merges two values concatenating all values - * into a flattened array - * @param {Any} [initial] - * @return {Function} - */ -export const flatConcatAtType = createTypeReducer(flatConcat); diff --git a/src/legacy/ui/ui_exports/ui_export_types/reduce/flat_concat_values_at_type.js b/src/legacy/ui/ui_exports/ui_export_types/reduce/flat_concat_values_at_type.js deleted file mode 100644 index 229c5be24aac5..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/reduce/flat_concat_values_at_type.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { createTypeReducer, flatConcat, mergeWith } from './lib'; - -/** - * Reducer that merges specs by concatenating the values of - * all keys in accumulator and spec with the same logic as concat - * @param {[type]} initial [description] - * @return {[type]} [description] - */ -export const flatConcatValuesAtType = createTypeReducer((objectA, objectB) => - mergeWith(objectA || {}, objectB || {}, flatConcat) -); diff --git a/src/legacy/ui/ui_exports/ui_export_types/reduce/index.js b/src/legacy/ui/ui_exports/ui_export_types/reduce/index.js deleted file mode 100644 index 7dc1ba60fb3cb..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/reduce/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { mergeAtType } from './merge_at_type'; -export { flatConcatValuesAtType } from './flat_concat_values_at_type'; -export { flatConcatAtType } from './flat_concat_at_type'; diff --git a/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/create_type_reducer.js b/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/create_type_reducer.js deleted file mode 100644 index bf4793c208308..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/create_type_reducer.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Creates a reducer that reduces the values within `acc[type]` by calling - * reducer with signature: - * - * reducer(acc[type], spec, type, pluginSpec) - * - * @param {Function} reducer - * @return {Function} - */ -export const createTypeReducer = (reducer) => (acc, spec, type, pluginSpec) => ({ - ...acc, - [type]: reducer(acc[type], spec, type, pluginSpec), -}); diff --git a/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/flat_concat.js b/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/flat_concat.js deleted file mode 100644 index 1337c8a85d5b4..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/flat_concat.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/** - * Concatenate two values into a single array, ignoring either - * value if it is undefined and flattening the value if it is an array - * @param {Array|T} a - * @param {Array} b - * @return {Array} - */ -export const flatConcat = (a, b) => [].concat(a === undefined ? [] : a, b === undefined ? [] : b); diff --git a/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/index.js b/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/index.js deleted file mode 100644 index e4281caebe245..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { flatConcat } from './flat_concat'; -export { mergeWith } from './merge_with'; -export { createTypeReducer } from './create_type_reducer'; diff --git a/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/merge_with.js b/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/merge_with.js deleted file mode 100644 index 6c7d31e6fd74d..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/merge_with.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const uniqueConcat = (arrayA, arrayB) => - arrayB.reduce((acc, key) => (acc.includes(key) ? acc : acc.concat(key)), arrayA); - -/** - * Assign the keys from both objA and objB to target after passing the - * current and new value through merge as `(target[key], source[key])` - * @param {Object} objA - * @param {Object} objB - * @param {Function} merge - * @return {Object} target - */ -export function mergeWith(objA, objB, merge) { - const target = {}; - const keys = uniqueConcat(Object.keys(objA), Object.keys(objB)); - for (const key of keys) { - target[key] = merge(objA[key], objB[key]); - } - return target; -} diff --git a/src/legacy/ui/ui_exports/ui_export_types/reduce/merge_at_type.js b/src/legacy/ui/ui_exports/ui_export_types/reduce/merge_at_type.js deleted file mode 100644 index 4f5a501253851..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/reduce/merge_at_type.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { createTypeReducer } from './lib'; - -export const mergeAtType = createTypeReducer((a, b) => ({ - ...a, - ...b, -})); diff --git a/src/legacy/ui/ui_exports/ui_export_types/saved_object.js b/src/legacy/ui/ui_exports/ui_export_types/saved_object.js deleted file mode 100644 index be6898d3e642c..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/saved_object.js +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { flatConcatAtType, mergeAtType } from './reduce'; -import { alias, mapSpec, uniqueKeys, wrap } from './modify_reduce'; - -// mapping types -export const mappings = wrap( - alias('savedObjectMappings'), - mapSpec((spec, type, pluginSpec) => ({ - pluginId: pluginSpec.getId(), - properties: spec, - })), - flatConcatAtType -); - -const pluginId = (pluginSpec) => (pluginSpec.id ? pluginSpec.id() : pluginSpec.getId()); - -// Combines the `migrations` property of each plugin, -// ensuring that properties are unique across plugins -// and has migrations defined where the mappings are defined. -// See saved_objects/migrations for more details. -export const migrations = wrap( - alias('savedObjectMigrations'), - (next) => (acc, spec, type, pluginSpec) => { - const mappings = pluginSpec.getExportSpecs().mappings || {}; - const invalidMigrationTypes = Object.keys(spec).filter((type) => !mappings[type]); - if (invalidMigrationTypes.length) { - throw new Error( - 'Migrations and mappings must be defined together in the uiExports of a single plugin. ' + - `${pluginId(pluginSpec)} defines migrations for types ${invalidMigrationTypes.join( - ', ' - )} but does not define their mappings.` - ); - } - return next(acc, spec, type, pluginSpec); - }, - uniqueKeys(), - mergeAtType -); - -export const savedObjectSchemas = wrap(uniqueKeys(), mergeAtType); - -export const savedObjectsManagement = wrap(uniqueKeys(), mergeAtType); - -// Combines the `validations` property of each plugin, -// ensuring that properties are unique across plugins. -// See saved_objects/validation for more details. -export const validations = wrap(alias('savedObjectValidations'), uniqueKeys(), mergeAtType); diff --git a/src/legacy/ui/ui_exports/ui_export_types/task_definitions.js b/src/legacy/ui/ui_exports/ui_export_types/task_definitions.js deleted file mode 100644 index 8a0ed85d86f3e..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/task_definitions.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { mergeAtType } from './reduce'; -import { alias, wrap, uniqueKeys } from './modify_reduce'; - -// How plugins define tasks that the task manager can run. -export const taskDefinitions = wrap(alias('taskDefinitions'), uniqueKeys(), mergeAtType); diff --git a/src/legacy/ui/ui_exports/ui_export_types/ui_nav_links.js b/src/legacy/ui/ui_exports/ui_export_types/ui_nav_links.js deleted file mode 100644 index 34aff7463a249..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/ui_nav_links.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { flatConcatAtType } from './reduce'; -import { wrap, alias } from './modify_reduce'; - -export const links = wrap(alias('navLinkSpecs'), flatConcatAtType); -export const link = wrap(alias('navLinkSpecs'), flatConcatAtType); diff --git a/src/legacy/ui/ui_exports/ui_export_types/ui_settings.js b/src/legacy/ui/ui_exports/ui_export_types/ui_settings.js deleted file mode 100644 index 8d88490579c21..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/ui_settings.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { mergeAtType } from './reduce'; -import { wrap, uniqueKeys } from './modify_reduce'; - -export const uiSettingDefaults = wrap(uniqueKeys(), mergeAtType); diff --git a/src/legacy/ui/ui_exports/ui_export_types/unknown.js b/src/legacy/ui/ui_exports/ui_export_types/unknown.js deleted file mode 100644 index a12a514d2e6bf..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/unknown.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { flatConcatAtType } from './reduce'; -import { wrap, alias, debug } from './modify_reduce'; - -export const unknown = wrap(debug, alias('unknown'), flatConcatAtType); diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index e3b7c1e0c3ff9..2983dbbc28667 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -67,115 +67,108 @@ export function uiRenderMixin(kbnServer, server, config) { }, }); - // register the bootstrap.js route after plugins are initialized so that we can - // detect if any default auth strategies were registered - kbnServer.afterPluginsInit(() => { - const authEnabled = !!server.auth.settings.default; - - server.route({ - path: '/bootstrap.js', - method: 'GET', - config: { - tags: ['api'], - auth: authEnabled ? { mode: 'try' } : false, - }, - async handler(request, h) { - const soClient = kbnServer.newPlatform.start.core.savedObjects.getScopedClient( - KibanaRequest.from(request) - ); - const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient(soClient); - - const darkMode = - !authEnabled || request.auth.isAuthenticated - ? await uiSettings.get('theme:darkMode') - : false; - - const themeVersion = - !authEnabled || request.auth.isAuthenticated - ? await uiSettings.get('theme:version') - : 'v7'; - - const themeTag = `${themeVersion === 'v7' ? 'v7' : 'v8'}${darkMode ? 'dark' : 'light'}`; - - const buildHash = server.newPlatform.env.packageInfo.buildNum; - const basePath = config.get('server.basePath'); - - const regularBundlePath = `${basePath}/${buildHash}/bundles`; - - const styleSheetPaths = [ - `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, - ...(darkMode - ? [ - themeVersion === 'v7' - ? `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.darkCssDistFilename}` - : `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.darkV8CssDistFilename}`, - `${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`, - `${basePath}/ui/legacy_dark_theme.css`, - ] - : [ - themeVersion === 'v7' - ? `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}` - : `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.lightV8CssDistFilename}`, - `${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`, - `${basePath}/ui/legacy_light_theme.css`, - ]), - ]; - - const kpUiPlugins = kbnServer.newPlatform.__internals.uiPlugins; - const kpPluginPublicPaths = new Map(); - const kpPluginBundlePaths = new Set(); - - // recursively iterate over the kpUiPlugin ids and their required bundles - // to populate kpPluginPublicPaths and kpPluginBundlePaths - (function readKpPlugins(ids) { - for (const id of ids) { - if (kpPluginPublicPaths.has(id)) { - continue; - } - - kpPluginPublicPaths.set(id, `${regularBundlePath}/plugin/${id}/`); - kpPluginBundlePaths.add(`${regularBundlePath}/plugin/${id}/${id}.plugin.js`); - readKpPlugins(kpUiPlugins.internal.get(id).requiredBundles); + const authEnabled = !!server.auth.settings.default; + server.route({ + path: '/bootstrap.js', + method: 'GET', + config: { + tags: ['api'], + auth: authEnabled ? { mode: 'try' } : false, + }, + async handler(request, h) { + const soClient = kbnServer.newPlatform.start.core.savedObjects.getScopedClient( + KibanaRequest.from(request) + ); + const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient(soClient); + + const darkMode = + !authEnabled || request.auth.isAuthenticated + ? await uiSettings.get('theme:darkMode') + : false; + + const themeVersion = + !authEnabled || request.auth.isAuthenticated ? await uiSettings.get('theme:version') : 'v7'; + + const themeTag = `${themeVersion === 'v7' ? 'v7' : 'v8'}${darkMode ? 'dark' : 'light'}`; + + const buildHash = server.newPlatform.env.packageInfo.buildNum; + const basePath = config.get('server.basePath'); + + const regularBundlePath = `${basePath}/${buildHash}/bundles`; + + const styleSheetPaths = [ + `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, + ...(darkMode + ? [ + themeVersion === 'v7' + ? `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.darkCssDistFilename}` + : `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.darkV8CssDistFilename}`, + `${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`, + `${basePath}/ui/legacy_dark_theme.css`, + ] + : [ + themeVersion === 'v7' + ? `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}` + : `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.lightV8CssDistFilename}`, + `${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`, + `${basePath}/ui/legacy_light_theme.css`, + ]), + ]; + + const kpUiPlugins = kbnServer.newPlatform.__internals.uiPlugins; + const kpPluginPublicPaths = new Map(); + const kpPluginBundlePaths = new Set(); + + // recursively iterate over the kpUiPlugin ids and their required bundles + // to populate kpPluginPublicPaths and kpPluginBundlePaths + (function readKpPlugins(ids) { + for (const id of ids) { + if (kpPluginPublicPaths.has(id)) { + continue; } - })(kpUiPlugins.public.keys()); - - const jsDependencyPaths = [ - ...UiSharedDeps.jsDepFilenames.map( - (filename) => `${regularBundlePath}/kbn-ui-shared-deps/${filename}` - ), - `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`, - - `${regularBundlePath}/core/core.entry.js`, - ...kpPluginBundlePaths, - ]; - - // These paths should align with the bundle routes configured in - // src/optimize/bundles_route/bundles_route.ts - const publicPathMap = JSON.stringify({ - core: `${regularBundlePath}/core/`, - 'kbn-ui-shared-deps': `${regularBundlePath}/kbn-ui-shared-deps/`, - ...Object.fromEntries(kpPluginPublicPaths), - }); - - const bootstrap = new AppBootstrap({ - templateData: { - themeTag, - jsDependencyPaths, - styleSheetPaths, - publicPathMap, - }, - }); - - const body = await bootstrap.getJsFile(); - const etag = await bootstrap.getJsFileHash(); - - return h - .response(body) - .header('cache-control', 'must-revalidate') - .header('content-type', 'application/javascript') - .etag(etag); - }, - }); + + kpPluginPublicPaths.set(id, `${regularBundlePath}/plugin/${id}/`); + kpPluginBundlePaths.add(`${regularBundlePath}/plugin/${id}/${id}.plugin.js`); + readKpPlugins(kpUiPlugins.internal.get(id).requiredBundles); + } + })(kpUiPlugins.public.keys()); + + const jsDependencyPaths = [ + ...UiSharedDeps.jsDepFilenames.map( + (filename) => `${regularBundlePath}/kbn-ui-shared-deps/${filename}` + ), + `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`, + + `${regularBundlePath}/core/core.entry.js`, + ...kpPluginBundlePaths, + ]; + + // These paths should align with the bundle routes configured in + // src/optimize/bundles_route/bundles_route.ts + const publicPathMap = JSON.stringify({ + core: `${regularBundlePath}/core/`, + 'kbn-ui-shared-deps': `${regularBundlePath}/kbn-ui-shared-deps/`, + ...Object.fromEntries(kpPluginPublicPaths), + }); + + const bootstrap = new AppBootstrap({ + templateData: { + themeTag, + jsDependencyPaths, + styleSheetPaths, + publicPathMap, + }, + }); + + const body = await bootstrap.getJsFile(); + const etag = await bootstrap.getJsFileHash(); + + return h + .response(body) + .header('cache-control', 'must-revalidate') + .header('content-type', 'application/javascript') + .etag(etag); + }, }); server.route({ @@ -191,19 +184,17 @@ export function uiRenderMixin(kbnServer, server, config) { }); async function renderApp(h) { - const app = { getId: () => 'core' }; const { http } = kbnServer.newPlatform.setup.core; const { savedObjects } = kbnServer.newPlatform.start.core; - const { rendering, legacy } = kbnServer.newPlatform.__internals; + const { rendering } = kbnServer.newPlatform.__internals; const req = KibanaRequest.from(h.request); const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient( savedObjects.getScopedClient(req) ); - const vars = await legacy.getVars(app.getId(), h.request, { + const vars = { apmConfig: getApmConfig(h.request.path), - }); + }; const content = await rendering.render(h.request, uiSettings, { - app, includeUserSettings: true, vars, }); diff --git a/x-pack/.gitignore b/x-pack/.gitignore index d73b6f64f036a..99e33dbb88e92 100644 --- a/x-pack/.gitignore +++ b/x-pack/.gitignore @@ -6,13 +6,9 @@ /test/functional/apps/reporting/reports/session /test/reporting/configs/failure_debug/ /plugins/reporting/.chromium/ -/legacy/plugins/reporting/.chromium/ -/legacy/plugins/reporting/.phantom/ /plugins/reporting/chromium/ /plugins/reporting/.phantom/ /.aws-config.json /.env /.kibana-plugin-helpers.dev.* -!/legacy/plugins/infra/**/target .cache -!/legacy/plugins/security_solution/**/target diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index a700781438706..b0124546944ae 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -9,8 +9,8 @@ "xpack.alerts": "plugins/alerts", "xpack.eventLog": "plugins/event_log", "xpack.alertingBuiltins": "plugins/alerting_builtins", - "xpack.apm": ["legacy/plugins/apm", "plugins/apm"], - "xpack.beatsManagement": ["legacy/plugins/beats_management", "plugins/beats_management"], + "xpack.apm": "plugins/apm", + "xpack.beatsManagement": "plugins/beats_management", "xpack.canvas": "plugins/canvas", "xpack.cloud": "plugins/cloud", "xpack.dashboard": "plugins/dashboard_enhanced", @@ -35,15 +35,15 @@ "xpack.lens": "plugins/lens", "xpack.licenseMgmt": "plugins/license_management", "xpack.licensing": "plugins/licensing", - "xpack.logstash": ["plugins/logstash", "legacy/plugins/logstash"], + "xpack.logstash": ["plugins/logstash"], "xpack.main": "legacy/plugins/xpack_main", - "xpack.maps": ["plugins/maps", "legacy/plugins/maps"], - "xpack.ml": ["plugins/ml", "legacy/plugins/ml"], + "xpack.maps": ["plugins/maps"], + "xpack.ml": ["plugins/ml"], "xpack.monitoring": ["plugins/monitoring"], "xpack.remoteClusters": "plugins/remote_clusters", "xpack.painlessLab": "plugins/painless_lab", "xpack.reporting": ["plugins/reporting"], - "xpack.rollupJobs": ["legacy/plugins/rollup", "plugins/rollup"], + "xpack.rollupJobs": ["plugins/rollup"], "xpack.searchProfiler": "plugins/searchprofiler", "xpack.security": "plugins/security", "xpack.server": "legacy/server", diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index e6f160ce8c654..eec7b0246d026 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -8,17 +8,15 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector const fileMockPath = `${kibanaDirectory}/src/dev/jest/mocks/file_mock.js`; return { rootDir, - roots: ['/plugins', '/legacy/plugins', '/legacy/server'], + roots: ['/plugins'], moduleFileExtensions: ['js', 'mjs', 'json', 'ts', 'tsx', 'node'], moduleNameMapper: { '@elastic/eui$': `${kibanaDirectory}/node_modules/@elastic/eui/test-env`, '@elastic/eui/lib/(.*)?': `${kibanaDirectory}/node_modules/@elastic/eui/test-env/$1`, '^fixtures/(.*)': `${kibanaDirectory}/src/fixtures/$1`, - 'uiExports/(.*)': fileMockPath, '^src/core/(.*)': `${kibanaDirectory}/src/core/$1`, '^src/legacy/(.*)': `${kibanaDirectory}/src/legacy/$1`, '^src/plugins/(.*)': `${kibanaDirectory}/src/plugins/$1`, - '^legacy/plugins/xpack_main/(.*);': `${xPackKibanaDirectory}/legacy/plugins/xpack_main/public/$1`, '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': fileMockPath, '\\.module.(css|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/css_module_mock.js`, '\\.(css|less|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/style_mock.js`, @@ -30,8 +28,6 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector '^(!!)?file-loader!': fileMockPath, }, collectCoverageFrom: [ - 'legacy/plugins/**/*.{js,mjs,jsx,ts,tsx}', - 'legacy/server/**/*.{js,mjs,jsx,ts,tsx}', 'plugins/**/*.{js,mjs,jsx,ts,tsx}', '!**/{__test__,__snapshots__,__examples__,integration_tests,tests}/**', '!**/*.test.{js,mjs,ts,tsx}', diff --git a/x-pack/index.js b/x-pack/index.js deleted file mode 100644 index cb68004c26d65..0000000000000 --- a/x-pack/index.js +++ /dev/null @@ -1,11 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { xpackMain } from './legacy/plugins/xpack_main'; - -module.exports = function (kibana) { - return [xpackMain(kibana)]; -}; diff --git a/x-pack/legacy/common/__tests__/poller.js b/x-pack/legacy/common/__tests__/poller.js deleted file mode 100644 index 24558502a8d02..0000000000000 --- a/x-pack/legacy/common/__tests__/poller.js +++ /dev/null @@ -1,240 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { Poller } from '../poller'; - -describe('Poller', () => { - const pollFrequencyInMillis = 20; - let functionToPoll; - let successFunction; - let errorFunction; - let poller; - let clock; - - beforeEach(() => { - clock = sinon.useFakeTimers(); - }); - - afterEach(() => { - clock.restore(); - if (poller) { - poller.stop(); - } - }); - - // Allowing the Poller to poll requires intimate knowledge of the inner workings of the Poller. - // We have to ensure that the Promises internal to the `_poll` method are resolved to queue up - // the next setTimeout before incrementing the clock. The order of this differs slightly when the - // `trailing` is set, hence the different `allowPoll` and `allowDelayPoll` functions. - const queueNextPoll = async () => { - await Promise.resolve(); - await Promise.resolve(); - }; - - const allowPoll = async (interval) => { - await queueNextPoll(); - clock.tick(interval); - }; - - const allowDelayPoll = async (interval) => { - clock.tick(interval); - await queueNextPoll(); - }; - - describe('start()', () => { - beforeEach(() => { - functionToPoll = sinon.spy(() => { - return Promise.resolve(42); - }); - successFunction = sinon.spy(); - errorFunction = sinon.spy(); - poller = new Poller({ - functionToPoll, - successFunction, - errorFunction, - pollFrequencyInMillis, - }); - }); - - describe(`when trailing isn't set`, () => { - it(`polls immediately`, () => { - poller.start(); - expect(functionToPoll.callCount).to.be(1); - }); - }); - - describe(`when trailing is set to true`, () => { - beforeEach(() => { - poller = new Poller({ - functionToPoll, - successFunction, - errorFunction, - pollFrequencyInMillis, - trailing: true, - }); - }); - - it('waits for pollFrequencyInMillis before polling', async () => { - poller.start(); - expect(functionToPoll.callCount).to.be(0); - allowDelayPoll(pollFrequencyInMillis); - expect(functionToPoll.callCount).to.be(1); - }); - }); - - it('polls the functionToPoll multiple times', async () => { - poller.start(); - await allowPoll(pollFrequencyInMillis * 2); - expect(functionToPoll.callCount).to.be.greaterThan(1); - }); - - describe('when the function to poll succeeds', () => { - it('calls the successFunction multiple times', async () => { - poller.start(); - await allowPoll(pollFrequencyInMillis * 2); - expect(successFunction.callCount).to.be.greaterThan(1); - expect(errorFunction.callCount).to.be(0); - }); - }); - - describe('when the function to poll fails', () => { - beforeEach(() => { - functionToPoll = sinon.spy(() => { - return Promise.reject(42); - }); - }); - - describe('when the continuePollingOnError option has not been set', () => { - beforeEach(() => { - poller = new Poller({ - functionToPoll, - successFunction, - errorFunction, - pollFrequencyInMillis, - }); - }); - - it('calls the errorFunction exactly once and polling is stopped', async () => { - poller.start(); - await allowPoll(pollFrequencyInMillis * 4); - expect(poller.isRunning()).to.be(false); - expect(successFunction.callCount).to.be(0); - expect(errorFunction.callCount).to.be(1); - }); - }); - - describe('when the continuePollingOnError option has been set to true', () => { - beforeEach(() => { - poller = new Poller({ - functionToPoll, - successFunction, - errorFunction, - pollFrequencyInMillis, - continuePollingOnError: true, - }); - }); - - it('calls the errorFunction multiple times', async () => { - poller.start(); - await allowPoll(pollFrequencyInMillis); - await allowPoll(pollFrequencyInMillis); - expect(successFunction.callCount).to.be(0); - expect(errorFunction.callCount).to.be.greaterThan(1); - }); - - describe('when pollFrequencyErrorMultiplier has been set', () => { - beforeEach(() => { - poller = new Poller({ - functionToPoll, - successFunction, - errorFunction, - pollFrequencyInMillis, - continuePollingOnError: true, - pollFrequencyErrorMultiplier: 2, - }); - }); - - it('waits for the multiplier * the pollFrequency', async () => { - poller.start(); - await queueNextPoll(); - expect(functionToPoll.callCount).to.be(1); - await allowPoll(pollFrequencyInMillis); - expect(functionToPoll.callCount).to.be(1); - await allowPoll(pollFrequencyInMillis); - expect(functionToPoll.callCount).to.be(2); - }); - }); - }); - }); - }); - - describe('isRunning()', () => { - beforeEach(() => { - functionToPoll = sinon.spy(() => { - return Promise.resolve(42); - }); - poller = new Poller({ - functionToPoll, - }); - }); - - it('returns true immediately after invoking start()', () => { - poller.start(); - expect(poller.isRunning()).to.be(true); - }); - - it('returns false after invoking stop', () => { - poller.start(); - poller.stop(); - expect(poller.isRunning()).to.be(false); - }); - }); - - describe('stop()', () => { - describe(`when successFunction isn't set`, () => { - beforeEach(() => { - functionToPoll = sinon.spy(() => { - return Promise.resolve(42); - }); - poller = new Poller({ - functionToPoll, - pollFrequencyInMillis, - }); - }); - - it(`doesn't poll again`, async () => { - poller.start(); - expect(functionToPoll.callCount).to.be(1); - poller.stop(); - await allowPoll(pollFrequencyInMillis); - expect(functionToPoll.callCount).to.be(1); - }); - }); - - describe(`when successFunction is a Promise`, () => { - beforeEach(() => { - functionToPoll = sinon.spy(() => { - return Promise.resolve(42); - }); - poller = new Poller({ - functionToPoll, - successFunction: Promise.resolve(), - pollFrequencyInMillis, - }); - }); - - it(`doesn't poll again when successFunction is a Promise`, async () => { - poller.start(); - expect(functionToPoll.callCount).to.be(1); - poller.stop(); - await allowPoll(pollFrequencyInMillis); - expect(functionToPoll.callCount).to.be(1); - }); - }); - }); -}); diff --git a/x-pack/legacy/common/constants/index.ts b/x-pack/legacy/common/constants/index.ts deleted file mode 100644 index 4db0f994fd47e..0000000000000 --- a/x-pack/legacy/common/constants/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { - LICENSE_STATUS_UNAVAILABLE, - LICENSE_STATUS_INVALID, - LICENSE_STATUS_EXPIRED, - LICENSE_STATUS_VALID, -} from './license_status'; - -export { - LICENSE_TYPE_BASIC, - LICENSE_TYPE_STANDARD, - LICENSE_TYPE_GOLD, - LICENSE_TYPE_PLATINUM, - LICENSE_TYPE_ENTERPRISE, - LICENSE_TYPE_TRIAL, - RANKED_LICENSE_TYPES, - LicenseType, -} from './license_types'; diff --git a/x-pack/legacy/common/constants/license_status.ts b/x-pack/legacy/common/constants/license_status.ts deleted file mode 100644 index 5fdfa08d73959..0000000000000 --- a/x-pack/legacy/common/constants/license_status.ts +++ /dev/null @@ -1,10 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export const LICENSE_STATUS_UNAVAILABLE = 'UNAVAILABLE'; -export const LICENSE_STATUS_INVALID = 'INVALID'; -export const LICENSE_STATUS_EXPIRED = 'EXPIRED'; -export const LICENSE_STATUS_VALID = 'VALID'; diff --git a/x-pack/legacy/common/constants/license_types.ts b/x-pack/legacy/common/constants/license_types.ts deleted file mode 100644 index 8c329df2f85f7..0000000000000 --- a/x-pack/legacy/common/constants/license_types.ts +++ /dev/null @@ -1,31 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export const LICENSE_TYPE_BASIC = 'basic'; -export const LICENSE_TYPE_STANDARD = 'standard'; -export const LICENSE_TYPE_GOLD = 'gold'; -export const LICENSE_TYPE_PLATINUM = 'platinum'; -export const LICENSE_TYPE_ENTERPRISE = 'enterprise'; -export const LICENSE_TYPE_TRIAL = 'trial'; - -export type LicenseType = - | typeof LICENSE_TYPE_BASIC - | typeof LICENSE_TYPE_STANDARD - | typeof LICENSE_TYPE_GOLD - | typeof LICENSE_TYPE_PLATINUM - | typeof LICENSE_TYPE_ENTERPRISE - | typeof LICENSE_TYPE_TRIAL; - -// These are ordered from least featureful to most featureful, so we can assume that someone holding -// a license at a particular index cannot access any features unlocked by the licenses that follow it. -export const RANKED_LICENSE_TYPES = [ - LICENSE_TYPE_BASIC, - LICENSE_TYPE_STANDARD, - LICENSE_TYPE_GOLD, - LICENSE_TYPE_PLATINUM, - LICENSE_TYPE_ENTERPRISE, - LICENSE_TYPE_TRIAL, -]; diff --git a/x-pack/legacy/common/eui_draggable/index.d.ts b/x-pack/legacy/common/eui_draggable/index.d.ts deleted file mode 100644 index 322966b3c982e..0000000000000 --- a/x-pack/legacy/common/eui_draggable/index.d.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiDraggable, EuiDragDropContext } from '@elastic/eui'; - -type PropsOf = T extends React.ComponentType ? ComponentProps : never; -type FirstArgumentOf = Func extends (arg1: infer FirstArgument, ...rest: any[]) => any - ? FirstArgument - : never; -export type DragHandleProps = FirstArgumentOf< - Exclude['children'], React.ReactElement> ->['dragHandleProps']; -export type DropResult = FirstArgumentOf['onDragEnd']>; diff --git a/x-pack/legacy/common/eui_styled_components/index.ts b/x-pack/legacy/common/eui_styled_components/index.ts deleted file mode 100644 index 9b3ed903627b4..0000000000000 --- a/x-pack/legacy/common/eui_styled_components/index.ts +++ /dev/null @@ -1,20 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - css, - euiStyled, - EuiTheme, - EuiThemeProvider, - createGlobalStyle, - keyframes, - withTheme, -} from './eui_styled_components'; - -export { css, euiStyled, EuiTheme, EuiThemeProvider, createGlobalStyle, keyframes, withTheme }; -// In order to to mimic the styled-components module we need to ignore the following -// eslint-disable-next-line import/no-default-export -export default euiStyled; diff --git a/x-pack/legacy/common/poller.d.ts b/x-pack/legacy/common/poller.d.ts deleted file mode 100644 index df39d93a28a81..0000000000000 --- a/x-pack/legacy/common/poller.d.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; - * you may not use this file except in compliance with the Elastic License. - */ - -export declare class Poller { - constructor(options: any); - - public start(): void; - public stop(): void; - public isRunning(): boolean; - public getPollFrequency(): number; -} diff --git a/x-pack/legacy/common/poller.js b/x-pack/legacy/common/poller.js deleted file mode 100644 index 09824ce9d6d23..0000000000000 --- a/x-pack/legacy/common/poller.js +++ /dev/null @@ -1,79 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import _ from 'lodash'; - -export class Poller { - constructor(options) { - this.functionToPoll = options.functionToPoll; // Must return a Promise - this.successFunction = options.successFunction || _.noop; - this.errorFunction = options.errorFunction || _.noop; - this.pollFrequencyInMillis = options.pollFrequencyInMillis; - this.trailing = options.trailing || false; - this.continuePollingOnError = options.continuePollingOnError || false; - this.pollFrequencyErrorMultiplier = options.pollFrequencyErrorMultiplier || 1; - this._timeoutId = null; - this._isRunning = false; - } - - getPollFrequency() { - return this.pollFrequencyInMillis; - } - - _poll() { - return this.functionToPoll() - .then(this.successFunction) - .then(() => { - if (!this._isRunning) { - return; - } - - this._timeoutId = setTimeout(this._poll.bind(this), this.pollFrequencyInMillis); - }) - .catch((e) => { - this.errorFunction(e); - if (!this._isRunning) { - return; - } - - if (this.continuePollingOnError) { - this._timeoutId = setTimeout( - this._poll.bind(this), - this.pollFrequencyInMillis * this.pollFrequencyErrorMultiplier - ); - } else { - this.stop(); - } - }); - } - - start() { - if (this._isRunning) { - return; - } - - this._isRunning = true; - if (this.trailing) { - this._timeoutId = setTimeout(this._poll.bind(this), this.pollFrequencyInMillis); - } else { - this._poll(); - } - } - - stop() { - if (!this._isRunning) { - return; - } - - this._isRunning = false; - clearTimeout(this._timeoutId); - this._timeoutId = null; - } - - isRunning() { - return this._isRunning; - } -} diff --git a/x-pack/legacy/plugins/xpack_main/index.js b/x-pack/legacy/plugins/xpack_main/index.js deleted file mode 100644 index a3bd66e744fda..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/index.js +++ /dev/null @@ -1,31 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { resolve } from 'path'; -import { setupXPackMain } from './server/lib/setup_xpack_main'; -import { xpackInfoRoute } from './server/routes/api/v1'; - -export const xpackMain = (kibana) => { - return new kibana.Plugin({ - id: 'xpack_main', - configPrefix: 'xpack.xpack_main', - publicDir: resolve(__dirname, 'public'), - require: [], - - config(Joi) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - - init(server) { - setupXPackMain(server); - - // register routes - xpackInfoRoute(server); - }, - }); -}; diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js deleted file mode 100644 index f49f44bed97a7..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js +++ /dev/null @@ -1,68 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { BehaviorSubject } from 'rxjs'; -import sinon from 'sinon'; -import { XPackInfo } from '../xpack_info'; -import { setupXPackMain } from '../setup_xpack_main'; - -describe('setupXPackMain()', () => { - const sandbox = sinon.createSandbox(); - - let mockServer; - let mockStatusObservable; - let mockElasticsearchPlugin; - - beforeEach(() => { - sandbox.useFakeTimers(); - - mockElasticsearchPlugin = { - getCluster: sinon.stub(), - }; - - mockStatusObservable = sinon.stub({ subscribe() {} }); - - mockServer = sinon.stub({ - plugins: { - elasticsearch: mockElasticsearchPlugin, - }, - newPlatform: { - setup: { - core: { - status: { - core$: { - pipe() { - return mockStatusObservable; - }, - }, - }, - }, - plugins: { features: {}, licensing: { license$: new BehaviorSubject() } }, - }, - }, - events: { on() {} }, - log() {}, - config() {}, - expose() {}, - ext() {}, - }); - - // Make sure plugins doesn't consume config - const configGetStub = sinon - .stub() - .throws(new Error('`config.get` is called with unexpected key.')); - mockServer.config.returns({ get: configGetStub }); - }); - - afterEach(() => sandbox.restore()); - - it('all extension hooks should be properly initialized.', () => { - setupXPackMain(mockServer); - - sinon.assert.calledWithExactly(mockServer.expose, 'info', sinon.match.instanceOf(XPackInfo)); - sinon.assert.calledWithExactly(mockStatusObservable.subscribe, sinon.match.func); - }); -}); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/xpack_info.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/xpack_info.js deleted file mode 100644 index 81fb822882817..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/xpack_info.js +++ /dev/null @@ -1,398 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { createHash } from 'crypto'; -import { BehaviorSubject } from 'rxjs'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { XPackInfo } from '../xpack_info'; -import { licensingMock } from '../../../../../../plugins/licensing/server/mocks'; - -function createLicense(license = {}, features = {}) { - return licensingMock.createLicense({ - license: { - uid: 'custom-uid', - type: 'gold', - mode: 'gold', - status: 'active', - expiryDateInMillis: 1286575200000, - ...license, - }, - features: { - security: { - description: 'Security for the Elastic Stack', - isAvailable: true, - isEnabled: true, - }, - watcher: { - description: 'Alerting, Notification and Automation for the Elastic Stack', - isAvailable: true, - isEnabled: false, - }, - ...features, - }, - }); -} - -function getSignature(object) { - return createHash('md5').update(JSON.stringify(object)).digest('hex'); -} - -describe('XPackInfo', () => { - let mockServer; - let mockElasticsearchPlugin; - - beforeEach(() => { - mockServer = sinon.stub({ - plugins: { elasticsearch: mockElasticsearchPlugin }, - events: { on() {} }, - newPlatform: { - setup: { - plugins: { - licensing: {}, - }, - }, - }, - }); - }); - - describe('refreshNow()', () => { - it('delegates to the new platform licensing plugin', async () => { - const refresh = sinon.spy(); - - const xPackInfo = new XPackInfo(mockServer, { - licensing: { - license$: new BehaviorSubject(createLicense()), - refresh: refresh, - }, - }); - - await xPackInfo.refreshNow(); - - sinon.assert.calledOnce(refresh); - }); - }); - - describe('license', () => { - let xPackInfo; - let license$; - beforeEach(async () => { - license$ = new BehaviorSubject(createLicense()); - xPackInfo = new XPackInfo(mockServer, { - licensing: { - license$, - refresh: () => null, - }, - }); - }); - - it('getUid() shows license uid returned from the license$.', async () => { - expect(xPackInfo.license.getUid()).to.be('custom-uid'); - - license$.next(createLicense({ uid: 'new-custom-uid' })); - - expect(xPackInfo.license.getUid()).to.be('new-custom-uid'); - - license$.next(createLicense({ uid: undefined, error: 'error-reason' })); - - expect(xPackInfo.license.getUid()).to.be(undefined); - }); - - it('isActive() is based on the status returned from the backend.', async () => { - expect(xPackInfo.license.isActive()).to.be(true); - - license$.next(createLicense({ status: 'expired' })); - expect(xPackInfo.license.isActive()).to.be(false); - - license$.next(createLicense({ status: 'some other value' })); - expect(xPackInfo.license.isActive()).to.be(false); - - license$.next(createLicense({ status: 'active' })); - expect(xPackInfo.license.isActive()).to.be(true); - - license$.next(createLicense({ status: undefined, error: 'error-reason' })); - expect(xPackInfo.license.isActive()).to.be(false); - }); - - it('getExpiryDateInMillis() is based on the value returned from the backend.', async () => { - expect(xPackInfo.license.getExpiryDateInMillis()).to.be(1286575200000); - - license$.next(createLicense({ expiryDateInMillis: 10203040 })); - expect(xPackInfo.license.getExpiryDateInMillis()).to.be(10203040); - - license$.next(createLicense({ expiryDateInMillis: undefined, error: 'error-reason' })); - expect(xPackInfo.license.getExpiryDateInMillis()).to.be(undefined); - }); - - it('getType() is based on the value returned from the backend.', async () => { - expect(xPackInfo.license.getType()).to.be('gold'); - - license$.next(createLicense({ type: 'basic' })); - expect(xPackInfo.license.getType()).to.be('basic'); - - license$.next(createLicense({ type: undefined, error: 'error-reason' })); - expect(xPackInfo.license.getType()).to.be(undefined); - }); - - it('isOneOf() correctly determines if current license is presented in the specified list.', async () => { - expect(xPackInfo.license.isOneOf('gold')).to.be(true); - expect(xPackInfo.license.isOneOf(['gold', 'basic'])).to.be(true); - expect(xPackInfo.license.isOneOf(['platinum', 'basic'])).to.be(false); - expect(xPackInfo.license.isOneOf('standard')).to.be(false); - - license$.next(createLicense({ mode: 'basic' })); - - expect(xPackInfo.license.isOneOf('basic')).to.be(true); - expect(xPackInfo.license.isOneOf(['gold', 'basic'])).to.be(true); - expect(xPackInfo.license.isOneOf(['platinum', 'gold'])).to.be(false); - expect(xPackInfo.license.isOneOf('standard')).to.be(false); - }); - }); - - describe('feature', () => { - let xPackInfo; - let license$; - beforeEach(async () => { - license$ = new BehaviorSubject( - createLicense( - {}, - { - feature: { - isAvailable: false, - isEnabled: true, - }, - } - ) - ); - xPackInfo = new XPackInfo(mockServer, { - licensing: { - license$, - refresh: () => null, - }, - }); - }); - - it('isAvailable() checks whether particular feature is available.', async () => { - const availableFeatureOne = xPackInfo.feature('security'); - const availableFeatureTwo = xPackInfo.feature('watcher'); - const unavailableFeatureOne = xPackInfo.feature('feature'); - const unavailableFeatureTwo = xPackInfo.feature('non-existing-feature'); - - expect(availableFeatureOne.isAvailable()).to.be(true); - expect(availableFeatureTwo.isAvailable()).to.be(true); - expect(unavailableFeatureOne.isAvailable()).to.be(false); - expect(unavailableFeatureTwo.isAvailable()).to.be(false); - }); - - it('isEnabled() checks whether particular feature is enabled.', async () => { - const enabledFeatureOne = xPackInfo.feature('security'); - const enabledFeatureTwo = xPackInfo.feature('feature'); - const disabledFeatureOne = xPackInfo.feature('watcher'); - const disabledFeatureTwo = xPackInfo.feature('non-existing-feature'); - - expect(enabledFeatureOne.isEnabled()).to.be(true); - expect(enabledFeatureTwo.isEnabled()).to.be(true); - expect(disabledFeatureOne.isEnabled()).to.be(false); - expect(disabledFeatureTwo.isEnabled()).to.be(false); - }); - - it('registerLicenseCheckResultsGenerator() allows to fill in XPack Info feature specific info.', async () => { - const securityFeature = xPackInfo.feature('security'); - const watcherFeature = xPackInfo.feature('watcher'); - - expect(xPackInfo.toJSON().features.security).to.be(undefined); - expect(xPackInfo.toJSON().features.watcher).to.be(undefined); - - securityFeature.registerLicenseCheckResultsGenerator((info) => { - return { - isXPackInfo: info instanceof XPackInfo, - license: info.license.getType(), - someCustomValue: 100500, - }; - }); - - expect(xPackInfo.toJSON().features.security).to.eql({ - isXPackInfo: true, - license: 'gold', - someCustomValue: 100500, - }); - expect(xPackInfo.toJSON().features.watcher).to.be(undefined); - - watcherFeature.registerLicenseCheckResultsGenerator((info) => { - return { - isXPackInfo: info instanceof XPackInfo, - license: info.license.getType(), - someAnotherCustomValue: 500100, - }; - }); - - expect(xPackInfo.toJSON().features.security).to.eql({ - isXPackInfo: true, - license: 'gold', - someCustomValue: 100500, - }); - expect(xPackInfo.toJSON().features.watcher).to.eql({ - isXPackInfo: true, - license: 'gold', - someAnotherCustomValue: 500100, - }); - - license$.next(createLicense({ type: 'platinum' })); - - expect(xPackInfo.toJSON().features.security).to.eql({ - isXPackInfo: true, - license: 'platinum', - someCustomValue: 100500, - }); - expect(xPackInfo.toJSON().features.watcher).to.eql({ - isXPackInfo: true, - license: 'platinum', - someAnotherCustomValue: 500100, - }); - }); - - it('getLicenseCheckResults() correctly returns feature specific info.', async () => { - const securityFeature = xPackInfo.feature('security'); - const watcherFeature = xPackInfo.feature('watcher'); - - expect(securityFeature.getLicenseCheckResults()).to.be(undefined); - expect(watcherFeature.getLicenseCheckResults()).to.be(undefined); - - securityFeature.registerLicenseCheckResultsGenerator((info) => { - return { - isXPackInfo: info instanceof XPackInfo, - license: info.license.getType(), - someCustomValue: 100500, - }; - }); - - expect(securityFeature.getLicenseCheckResults()).to.eql({ - isXPackInfo: true, - license: 'gold', - someCustomValue: 100500, - }); - expect(watcherFeature.getLicenseCheckResults()).to.be(undefined); - - watcherFeature.registerLicenseCheckResultsGenerator((info) => { - return { - isXPackInfo: info instanceof XPackInfo, - license: info.license.getType(), - someAnotherCustomValue: 500100, - }; - }); - - expect(securityFeature.getLicenseCheckResults()).to.eql({ - isXPackInfo: true, - license: 'gold', - someCustomValue: 100500, - }); - expect(watcherFeature.getLicenseCheckResults()).to.eql({ - isXPackInfo: true, - license: 'gold', - someAnotherCustomValue: 500100, - }); - - license$.next(createLicense({ type: 'platinum' })); - - expect(securityFeature.getLicenseCheckResults()).to.eql({ - isXPackInfo: true, - license: 'platinum', - someCustomValue: 100500, - }); - expect(watcherFeature.getLicenseCheckResults()).to.eql({ - isXPackInfo: true, - license: 'platinum', - someAnotherCustomValue: 500100, - }); - }); - }); - - it('onLicenseInfoChange() allows to subscribe to license update', async () => { - const license$ = new BehaviorSubject(createLicense()); - - const xPackInfo = new XPackInfo(mockServer, { - licensing: { - license$, - refresh: () => null, - }, - }); - - const watcherFeature = xPackInfo.feature('watcher'); - watcherFeature.registerLicenseCheckResultsGenerator((info) => ({ - type: info.license.getType(), - })); - - const statuses = []; - xPackInfo.onLicenseInfoChange(() => statuses.push(watcherFeature.getLicenseCheckResults())); - - license$.next(createLicense({ type: 'basic' })); - expect(statuses).to.eql([{ type: 'basic' }]); - - license$.next(createLicense({ type: 'trial' })); - expect(statuses).to.eql([{ type: 'basic' }, { type: 'trial' }]); - }); - - it('refreshNow() leads to onLicenseInfoChange()', async () => { - const license$ = new BehaviorSubject(createLicense()); - - const xPackInfo = new XPackInfo(mockServer, { - licensing: { - license$, - refresh: () => license$.next({ type: 'basic' }), - }, - }); - - const watcherFeature = xPackInfo.feature('watcher'); - - watcherFeature.registerLicenseCheckResultsGenerator((info) => ({ - type: info.license.getType(), - })); - - const statuses = []; - xPackInfo.onLicenseInfoChange(() => statuses.push(watcherFeature.getLicenseCheckResults())); - - await xPackInfo.refreshNow(); - expect(statuses).to.eql([{ type: 'basic' }]); - }); - - it('getSignature() returns correct signature.', async () => { - const license$ = new BehaviorSubject(createLicense()); - const xPackInfo = new XPackInfo(mockServer, { - licensing: { - license$, - refresh: () => null, - }, - }); - - expect(xPackInfo.getSignature()).to.be( - getSignature({ - license: { - type: 'gold', - isActive: true, - expiryDateInMillis: 1286575200000, - }, - features: {}, - }) - ); - - license$.next(createLicense({ type: 'platinum', expiryDateInMillis: 20304050 })); - - const expectedSignature = getSignature({ - license: { - type: 'platinum', - isActive: true, - expiryDateInMillis: 20304050, - }, - features: {}, - }); - expect(xPackInfo.getSignature()).to.be(expectedSignature); - - // Should stay the same after refresh if nothing changed. - license$.next(createLicense({ type: 'platinum', expiryDateInMillis: 20304050 })); - - expect(xPackInfo.getSignature()).to.be(expectedSignature); - }); -}); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js b/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js deleted file mode 100644 index fd4e3c86d0ca7..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js +++ /dev/null @@ -1,33 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { pairwise } from 'rxjs/operators'; -import { XPackInfo } from './xpack_info'; - -/** - * Setup the X-Pack Main plugin. This is fired every time that the Elasticsearch plugin becomes Green. - * - * This will ensure that X-Pack is installed on the Elasticsearch cluster, as well as trigger the initial - * polling for _xpack/info. - * - * @param server {Object} The Kibana server object. - */ -export function setupXPackMain(server) { - const info = new XPackInfo(server, { licensing: server.newPlatform.setup.plugins.licensing }); - - server.expose('info', info); - - // trigger an xpack info refresh whenever the elasticsearch plugin status changes - server.newPlatform.setup.core.status.core$ - .pipe(pairwise()) - .subscribe(async ([coreLast, coreCurrent]) => { - if (coreLast.elasticsearch.level !== coreCurrent.elasticsearch.level) { - await info.refreshNow(); - } - }); - - return info; -} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.ts b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.ts deleted file mode 100644 index aa66532a2897d..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.ts +++ /dev/null @@ -1,240 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { createHash } from 'crypto'; -import { Legacy } from 'kibana'; - -import { XPackInfoLicense } from './xpack_info_license'; - -import { LicensingPluginSetup, ILicense } from '../../../../../plugins/licensing/server'; - -export interface XPackInfoOptions { - clusterSource?: string; - pollFrequencyInMillis: number; -} - -type LicenseGeneratorCheck = (xpackInfo: XPackInfo) => any; - -export interface XPackFeature { - isAvailable(): boolean; - isEnabled(): boolean; - registerLicenseCheckResultsGenerator(generator: LicenseGeneratorCheck): void; - getLicenseCheckResults(): any; -} - -interface Deps { - licensing: LicensingPluginSetup; -} - -/** - * A helper that provides a convenient way to access XPack Info returned by Elasticsearch. - */ -export class XPackInfo { - /** - * XPack License object. - * @type {XPackInfoLicense} - * @private - */ - _license: XPackInfoLicense; - - /** - * Feature name <-> feature license check generator function mapping. - * @type {Map} - * @private - */ - _featureLicenseCheckResultsGenerators = new Map(); - - /** - * Set of listener functions that will be called whenever the license - * info changes - * @type {Set} - */ - _licenseInfoChangedListeners = new Set<() => void>(); - - /** - * Cache that may contain last xpack info API response or error, json representation - * of xpack info and xpack info signature. - * @type {{response: Object|undefined, error: Object|undefined, json: Object|undefined, signature: string|undefined}} - * @private - */ - private _cache: { - license?: ILicense; - error?: string; - json?: Record; - signature?: string; - }; - - /** - * XPack License instance. - * @returns {XPackInfoLicense} - */ - public get license() { - return this._license; - } - - private readonly licensingPlugin: LicensingPluginSetup; - - /** - * Constructs XPack info object. - * @param {Hapi.Server} server HapiJS server instance. - */ - constructor(server: Legacy.Server, deps: Deps) { - if (!deps.licensing) { - throw new Error('XPackInfo requires enabled Licensing plugin'); - } - this.licensingPlugin = deps.licensing; - - this._cache = {}; - - this.licensingPlugin.license$.subscribe((license: ILicense) => { - if (license.isActive) { - this._cache = { - license, - error: undefined, - }; - } else { - this._cache = { - license, - error: license.error, - }; - } - - this._licenseInfoChangedListeners.forEach((fn) => fn()); - }); - - this._license = new XPackInfoLicense(() => this._cache.license); - } - - /** - * Checks whether XPack info is available. - * @returns {boolean} - */ - isAvailable() { - return Boolean(this._cache.license?.isAvailable); - } - - /** - * Checks whether ES was available - * @returns {boolean} - */ - isXpackUnavailable() { - return ( - this._cache.error && - this._cache.error === 'X-Pack plugin is not installed on the Elasticsearch cluster.' - ); - } - - /** - * If present, describes the reason why XPack info is not available. - * @returns {Error|string} - */ - unavailableReason() { - return this._cache.license?.getUnavailableReason(); - } - - onLicenseInfoChange(handler: () => void) { - this._licenseInfoChangedListeners.add(handler); - } - - /** - * Queries server to get the updated XPack info. - * @returns {Promise.} - */ - async refreshNow() { - await this.licensingPlugin.refresh(); - return this; - } - - /** - * Returns a wrapper around XPack info that gives an access to the properties of - * the specific feature. - * @param {string} name Name of the feature to get a wrapper for. - * @returns {Object} - */ - feature(name: string): XPackFeature { - return { - /** - * Checks whether feature is available (permitted by the current license). - * @returns {boolean} - */ - isAvailable: () => { - return Boolean(this._cache.license?.getFeature(name).isAvailable); - }, - - /** - * Checks whether feature is enabled (not disabled by the configuration specifically). - * @returns {boolean} - */ - isEnabled: () => { - return Boolean(this._cache.license?.getFeature(name).isEnabled); - }, - - /** - * Registers a `generator` function that will be called with XPackInfo instance as - * argument whenever XPack info changes. Whatever `generator` returns will be stored - * in XPackInfo JSON representation and can be accessed with `getLicenseCheckResults`. - * @param {Function} generator Function to call whenever XPackInfo changes. - */ - registerLicenseCheckResultsGenerator: (generator: LicenseGeneratorCheck) => { - this._featureLicenseCheckResultsGenerators.set(name, generator); - - // Since JSON representation and signature are cached we should invalidate them to - // include results from newly registered generator when they are requested. - this._cache.json = undefined; - this._cache.signature = undefined; - }, - - /** - * Returns license check results that were previously produced by the `generator` function. - * @returns {Object} - */ - getLicenseCheckResults: () => this.toJSON().features[name], - }; - } - - /** - * Extracts string md5 hash from the stringified version of license JSON representation. - * @returns {string} - */ - getSignature() { - if (this._cache.signature) { - return this._cache.signature; - } - - this._cache.signature = createHash('md5').update(JSON.stringify(this.toJSON())).digest('hex'); - - return this._cache.signature; - } - - /** - * Returns JSON representation of the license object that is suitable for serialization. - * @returns {Object} - */ - toJSON() { - if (this._cache.json) { - return this._cache.json; - } - - this._cache.json = { - license: { - type: this.license.getType(), - isActive: this.license.isActive(), - expiryDateInMillis: this.license.getExpiryDateInMillis(), - }, - features: {}, - }; - - // Set response elements specific to each feature. To do this, - // call the license check results generator for each feature, passing them - // the xpack info object - for (const [feature, licenseChecker] of this._featureLicenseCheckResultsGenerators) { - // return value expected to be a dictionary object. - this._cache.json.features[feature] = licenseChecker(this); - } - - return this._cache.json; - } -} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.test.js b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.test.js deleted file mode 100644 index ccb5742216ca7..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.test.js +++ /dev/null @@ -1,207 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { licensingMock } from '../../../../../plugins/licensing/server/mocks'; -import { XPackInfoLicense } from './xpack_info_license'; - -function getXPackInfoLicense(getRawLicense) { - return new XPackInfoLicense(getRawLicense); -} - -describe('XPackInfoLicense', () => { - const xpackInfoLicenseUndefined = getXPackInfoLicense(() => {}); - let xpackInfoLicense; - let getRawLicense; - - beforeEach(() => { - getRawLicense = jest.fn(); - xpackInfoLicense = getXPackInfoLicense(getRawLicense); - }); - - test('getUid returns uid field', () => { - const uid = 'abc123'; - - getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { uid } })); - - expect(xpackInfoLicense.getUid()).toBe(uid); - expect(getRawLicense).toHaveBeenCalledTimes(1); - - expect(xpackInfoLicenseUndefined.getUid()).toBe(undefined); - }); - - test('isActive returns true if status is active', () => { - getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'active' } })); - - expect(xpackInfoLicense.isActive()).toBe(true); - expect(getRawLicense).toHaveBeenCalledTimes(1); - }); - - test('isActive returns false if status is not active', () => { - getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'aCtIvE' } })); // needs to match exactly - - expect(xpackInfoLicense.isActive()).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(1); - - expect(xpackInfoLicenseUndefined.isActive()).toBe(false); - }); - - test('getExpiryDateInMillis returns expiry_date_in_millis', () => { - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { expiryDateInMillis: 123 } }) - ); - - expect(xpackInfoLicense.getExpiryDateInMillis()).toBe(123); - expect(getRawLicense).toHaveBeenCalledTimes(1); - - expect(xpackInfoLicenseUndefined.getExpiryDateInMillis()).toBe(undefined); - }); - - test('isOneOf returns true of the mode includes one of the types', () => { - getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { mode: 'platinum' } })); - - expect(xpackInfoLicense.isOneOf('platinum')).toBe(true); - expect(getRawLicense).toHaveBeenCalledTimes(1); - - expect(xpackInfoLicense.isOneOf(['platinum'])).toBe(true); - expect(getRawLicense).toHaveBeenCalledTimes(2); - expect(xpackInfoLicense.isOneOf(['gold', 'platinum'])).toBe(true); - expect(getRawLicense).toHaveBeenCalledTimes(3); - expect(xpackInfoLicense.isOneOf(['platinum', 'gold'])).toBe(true); - expect(getRawLicense).toHaveBeenCalledTimes(4); - expect(xpackInfoLicense.isOneOf(['basic', 'gold'])).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(5); - expect(xpackInfoLicense.isOneOf(['basic'])).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(6); - - expect(xpackInfoLicenseUndefined.isOneOf(['platinum', 'gold'])).toBe(false); - }); - - test('getType returns the type', () => { - getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { type: 'basic' } })); - - expect(xpackInfoLicense.getType()).toBe('basic'); - expect(getRawLicense).toHaveBeenCalledTimes(1); - - getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { type: 'gold' } })); - - expect(xpackInfoLicense.getType()).toBe('gold'); - expect(getRawLicense).toHaveBeenCalledTimes(2); - - expect(xpackInfoLicenseUndefined.getType()).toBe(undefined); - }); - - test('getMode returns the mode', () => { - getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { mode: 'basic' } })); - - expect(xpackInfoLicense.getMode()).toBe('basic'); - expect(getRawLicense).toHaveBeenCalledTimes(1); - - getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { mode: 'gold' } })); - - expect(xpackInfoLicense.getMode()).toBe('gold'); - expect(getRawLicense).toHaveBeenCalledTimes(2); - - expect(xpackInfoLicenseUndefined.getMode()).toBe(undefined); - }); - - test('isActiveLicense returns the true if active and typeChecker matches', () => { - const expectAbc123 = (type) => type === 'abc123'; - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'active', mode: 'abc123' } }) - ); - - expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(true); - expect(getRawLicense).toHaveBeenCalledTimes(1); - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'abc123' } }) - ); - - expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(2); - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'NOTabc123' } }) - ); - - expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(3); - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'active', mode: 'NOTabc123' } }) - ); - - expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(4); - - expect(xpackInfoLicenseUndefined.isActive(expectAbc123)).toBe(false); - }); - - test('isBasic returns the true if active and basic', () => { - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'active', mode: 'basic' } }) - ); - - expect(xpackInfoLicense.isBasic()).toBe(true); - expect(getRawLicense).toHaveBeenCalledTimes(1); - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'gold' } }) - ); - - expect(xpackInfoLicense.isBasic()).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(2); - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'trial' } }) - ); - - expect(xpackInfoLicense.isBasic()).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(3); - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'active', mode: 'platinum' } }) - ); - - expect(xpackInfoLicense.isBasic()).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(4); - - expect(xpackInfoLicenseUndefined.isBasic()).toBe(false); - }); - - test('isNotBasic returns the true if active and not basic', () => { - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'active', mode: 'platinum' } }) - ); - - expect(xpackInfoLicense.isNotBasic()).toBe(true); - expect(getRawLicense).toHaveBeenCalledTimes(1); - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'gold' } }) - ); - - expect(xpackInfoLicense.isNotBasic()).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(2); - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'trial' } }) - ); - - expect(xpackInfoLicense.isNotBasic()).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(3); - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'active', mode: 'basic' } }) - ); - - expect(xpackInfoLicense.isNotBasic()).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(4); - - expect(xpackInfoLicenseUndefined.isNotBasic()).toBe(false); - }); -}); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.ts b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.ts deleted file mode 100644 index dd53f63909475..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.ts +++ /dev/null @@ -1,111 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ILicense } from '../../../../../plugins/licensing/server'; - -/** - * "View" for XPack Info license information. - */ -export class XPackInfoLicense { - /** - * Function that retrieves license information from the XPack info object. - * @type {Function} - * @private - */ - _getRawLicense: () => ILicense | undefined; - - constructor(getRawLicense: () => ILicense | undefined) { - this._getRawLicense = getRawLicense; - } - - /** - * Returns unique identifier of the license. - * @returns {string|undefined} - */ - getUid() { - return this._getRawLicense()?.uid; - } - - /** - * Indicates whether license is still active. - * @returns {boolean} - */ - isActive() { - return Boolean(this._getRawLicense()?.isActive); - } - - /** - * Returns license expiration date in ms. - * - * Note: A basic license created after 6.3 will have no expiration, thus returning undefined. - * - * @returns {number|undefined} - */ - getExpiryDateInMillis() { - return this._getRawLicense()?.expiryDateInMillis; - } - - /** - * Checks if the license is represented in a specified license list. - * @param {String} candidateLicenses List of the licenses to check against. - * @returns {boolean} - */ - isOneOf(candidateLicenses: string | string[]) { - const candidates = Array.isArray(candidateLicenses) ? candidateLicenses : [candidateLicenses]; - const mode = this._getRawLicense()?.mode; - return Boolean(mode && candidates.includes(mode)); - } - - /** - * Returns type of the license (basic, gold etc.). - * @returns {string|undefined} - */ - getType() { - return this._getRawLicense()?.type; - } - - /** - * Returns mode of the license (basic, gold etc.). This is the "effective" type of the license. - * @returns {string|undefined} - */ - getMode() { - return this._getRawLicense()?.mode; - } - - /** - * Determine if the current license is active and the supplied {@code type}. - * - * @param {Function} typeChecker The license type checker. - * @returns {boolean} - */ - isActiveLicense(typeChecker: (mode: string) => boolean) { - const license = this._getRawLicense(); - - return Boolean(license?.isActive && typeChecker(license.mode as any)); - } - - /** - * Determine if the license is an active, basic license. - * - * Note: This also verifies that the license is active. Therefore it is not safe to assume that !isBasic() === isNotBasic(). - * - * @returns {boolean} - */ - isBasic() { - return this.isActiveLicense((mode) => mode === 'basic'); - } - - /** - * Determine if the license is an active, non-basic license (e.g., standard, gold, platinum, or trial). - * - * Note: This also verifies that the license is active. Therefore it is not safe to assume that !isBasic() === isNotBasic(). - * - * @returns {boolean} - */ - isNotBasic() { - return this.isActiveLicense((mode) => mode !== 'basic'); - } -} diff --git a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/__tests__/xpack_info.js b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/__tests__/xpack_info.js deleted file mode 100644 index 540d9f63ea6c8..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/__tests__/xpack_info.js +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -import { xpackInfoRoute } from '../xpack_info'; - -describe('XPackInfo routes', () => { - let serverStub; - beforeEach(() => { - serverStub = { - route: sinon.stub(), - plugins: { - xpack_main: { - info: sinon.stub({ isAvailable() {}, toJSON() {} }), - }, - }, - }; - - xpackInfoRoute(serverStub); - }); - - it('correctly initialize XPack Info route.', () => { - sinon.assert.calledWithExactly(serverStub.route, { - method: 'GET', - path: '/api/xpack/v1/info', - handler: sinon.match.func, - }); - }); - - it('replies with `Not Found` Boom error if `xpackInfo` is not available.', () => { - serverStub.plugins.xpack_main.info.isAvailable.returns(false); - - const onRouteHandler = serverStub.route.firstCall.args[0].handler; - const response = onRouteHandler(); - - expect(response.isBoom).to.be(true); - expect(response.message).to.be('Not Found'); - expect(response.output.statusCode).to.be(404); - }); - - it('replies with pre-processed `xpackInfo` if it is available.', () => { - serverStub.plugins.xpack_main.info.isAvailable.returns(true); - serverStub.plugins.xpack_main.info.toJSON.returns({ - license: { - type: 'gold', - isActive: true, - expiryDateInMillis: 1509368280381, - }, - features: { - security: { - showLogin: true, - allowLogin: true, - showLinks: false, - allowRoleDocumentLevelSecurity: false, - allowRoleFieldLevelSecurity: false, - }, - }, - }); - - const onRouteHandler = serverStub.route.firstCall.args[0].handler; - const response = onRouteHandler(); - - expect(response).to.eql({ - license: { - type: 'gold', - is_active: true, - expiry_date_in_millis: 1509368280381, - }, - features: { - security: { - show_login: true, - allow_login: true, - show_links: false, - allow_role_document_level_security: false, - allow_role_field_level_security: false, - }, - }, - }); - }); -}); diff --git a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/xpack_info.js b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/xpack_info.js deleted file mode 100644 index 3cc57ae9fcab4..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/xpack_info.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; -import { convertKeysToSnakeCaseDeep } from '../../../../../../server/lib/key_case_converter'; - -/* - * A route to provide the basic XPack info for the production cluster - */ -export function xpackInfoRoute(server) { - server.route({ - method: 'GET', - path: '/api/xpack/v1/info', - handler() { - const xPackInfo = server.plugins.xpack_main.info; - - return xPackInfo.isAvailable() - ? convertKeysToSnakeCaseDeep(xPackInfo.toJSON()) - : Boom.notFound(); - }, - }); -} diff --git a/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts b/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts deleted file mode 100644 index c2ec5662ad12e..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import KbnServer from 'src/legacy/server/kbn_server'; -import { KibanaFeature } from '../../../../plugins/features/server'; -import { XPackInfo, XPackInfoOptions } from './lib/xpack_info'; -export { XPackFeature } from './lib/xpack_info'; - -export interface XPackMainPlugin { - info: XPackInfo; -} diff --git a/x-pack/legacy/server/lib/__tests__/key_case_converter.js b/x-pack/legacy/server/lib/__tests__/key_case_converter.js deleted file mode 100644 index 7ed9fa668ae66..0000000000000 --- a/x-pack/legacy/server/lib/__tests__/key_case_converter.js +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { convertKeysToSnakeCaseDeep, convertKeysToCamelCaseDeep } from '../key_case_converter'; - -describe('key_case_converter', () => { - let testObject; - - beforeEach(() => { - testObject = { - topLevelKey1: { - innerLevelKey1: 17, - inner_level_key2: [19, 31], - }, - top_level_key2: { - innerLevelKey1: 'foo_fooFoo', - inner_level_key2: [{ foo_bar: 29 }, { barBar: 37 }], - }, - }; - }); - - describe('convertKeysToSnakeCaseDeep', () => { - it('should recursively convert camelCase keys to snake_case keys', () => { - const expectedResultObject = { - top_level_key_1: { - inner_level_key_1: 17, - inner_level_key_2: [19, 31], - }, - top_level_key_2: { - inner_level_key_1: 'foo_fooFoo', - inner_level_key_2: [{ foo_bar: 29 }, { bar_bar: 37 }], - }, - }; - expect(convertKeysToSnakeCaseDeep(testObject)).to.eql(expectedResultObject); - }); - - it('should not modify original object', () => { - convertKeysToSnakeCaseDeep(testObject); - expect(Object.keys(testObject)).to.contain('topLevelKey1'); - expect(Object.keys(testObject.topLevelKey1)).to.contain('innerLevelKey1'); - }); - - it('should preserve inner arrays', () => { - const result = convertKeysToSnakeCaseDeep(testObject); - expect(testObject.topLevelKey1.inner_level_key2).to.be.an(Array); - expect(result.top_level_key_1.inner_level_key_2).to.be.an(Array); - }); - - it('should preserve top-level arrays', () => { - testObject = [{ foo_bar: 17 }, [19, { barBaz: 'qux' }]]; - const expectedResultObject = [{ foo_bar: 17 }, [19, { bar_baz: 'qux' }]]; - const result = convertKeysToSnakeCaseDeep(testObject); - expect(testObject).to.be.an(Array); - expect(testObject[1]).to.be.an(Array); - expect(result).to.be.an(Array); - expect(result[1]).to.be.an(Array); - expect(result).to.eql(expectedResultObject); - }); - - it('should throw an error if something other an object or array is passed in', () => { - const expectedErrorMessageRegexp = /Specified object should be an Object or Array/; - expect(convertKeysToSnakeCaseDeep) - .withArgs('neither an object nor an array') - .to.throwException(expectedErrorMessageRegexp); - }); - }); - - describe('convertKeysToCamelCaseDeep', () => { - it('should recursively convert snake_case keys to camelCase keys', () => { - const expectedResultObject = { - topLevelKey1: { - innerLevelKey1: 17, - innerLevelKey2: [19, 31], - }, - topLevelKey2: { - innerLevelKey1: 'foo_fooFoo', - innerLevelKey2: [{ fooBar: 29 }, { barBar: 37 }], - }, - }; - expect(convertKeysToCamelCaseDeep(testObject)).to.eql(expectedResultObject); - }); - - it('should not modify original object', () => { - convertKeysToCamelCaseDeep(testObject); - expect(Object.keys(testObject)).to.contain('top_level_key2'); - expect(Object.keys(testObject.topLevelKey1)).to.contain('inner_level_key2'); - }); - - it('should preserve inner arrays', () => { - const result = convertKeysToCamelCaseDeep(testObject); - expect(testObject.topLevelKey1.inner_level_key2).to.be.an(Array); - expect(result.topLevelKey1.innerLevelKey2).to.be.an(Array); - }); - - it('should preserve top-level arrays', () => { - testObject = [{ foo_bar: 17 }, [19, { barBaz: 'qux' }]]; - const expectedResultObject = [{ fooBar: 17 }, [19, { barBaz: 'qux' }]]; - const result = convertKeysToCamelCaseDeep(testObject); - expect(testObject).to.be.an(Array); - expect(testObject[1]).to.be.an(Array); - expect(result).to.be.an(Array); - expect(result[1]).to.be.an(Array); - expect(result).to.eql(expectedResultObject); - }); - - it('should throw an error if something other an object or array is passed in', () => { - const expectedErrorMessageRegexp = /Specified object should be an Object or Array/; - expect(convertKeysToCamelCaseDeep) - .withArgs('neither an object nor an array') - .to.throwException(expectedErrorMessageRegexp); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/__tests__/kibana_state.js b/x-pack/legacy/server/lib/__tests__/kibana_state.js deleted file mode 100644 index d1b4142b10446..0000000000000 --- a/x-pack/legacy/server/lib/__tests__/kibana_state.js +++ /dev/null @@ -1,129 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import rison from 'rison-node'; -import { parseKibanaState } from '../parse_kibana_state'; - -const stateIndices = { - global: '_g', - app: '_a', -}; -const globalTime = - '(refreshInterval:(display:Off,pause:!f,value:0),time:(from:now-15m,mode:quick,to:now))'; - -describe('Kibana state', function () { - describe('type checking', function () { - it('should throw if not given an object', function () { - const fn = () => parseKibanaState('i am not an object', 'global'); - const fn2 = () => parseKibanaState(['arrays are not valid either'], 'global'); - expect(fn).to.throwException(/must be an object/i); - expect(fn2).to.throwException(/must be an object/i); - }); - - it('should throw with invalid type', function () { - const fn = () => parseKibanaState({}, 'this is an invalid state type'); - expect(fn).to.throwException(/unknown state type/i); - }); - }); - - describe('value of exists', function () { - it('should be false if state does not exist', function () { - const state = parseKibanaState({}, 'global'); - expect(state.exists).to.equal(false); - }); - - it('should be true if state exists', function () { - const query = {}; - query[stateIndices.global] = rison.encode({ hello: 'world' }); - const state = parseKibanaState(query, 'global'); - expect(state.exists).to.equal(true); - }); - }); - - describe('instance methods', function () { - let query; - - beforeEach(function () { - query = {}; - query[stateIndices.global] = globalTime; - }); - - describe('get', function () { - it('should return the value', function () { - const state = parseKibanaState(query, 'global'); - const { refreshInterval } = rison.decode(globalTime); - expect(state.get('refreshInterval')).to.eql(refreshInterval); - }); - - it('should use the default value for missing props', function () { - const defaultValue = 'default value'; - const state = parseKibanaState(query, 'global'); - expect(state.get('no such value', defaultValue)).to.equal(defaultValue); - }); - }); - - describe('set', function () { - it('should update the value of the state', function () { - const state = parseKibanaState(query, 'global'); - expect(state.get('refreshInterval.pause')).to.equal(false); - - state.set(['refreshInterval', 'pause'], true); - expect(state.get('refreshInterval.pause')).to.equal(true); - }); - - it('should create new properties', function () { - const prop = 'newProp'; - const value = 12345; - const state = parseKibanaState(query, 'global'); - expect(state.get(prop)).to.be(undefined); - - state.set(prop, value); - expect(state.get(prop)).to.not.be(undefined); - expect(state.get(prop)).to.equal(value); - }); - }); - - describe('removing properties', function () { - it('should remove a single value', function () { - const state = parseKibanaState(query, 'global'); - expect(state.get('refreshInterval')).to.be.an('object'); - - state.removeProps('refreshInterval'); - expect(state.get('refreshInterval')).to.be(undefined); - }); - - it('should remove multiple values', function () { - const state = parseKibanaState(query, 'global'); - expect(state.get('refreshInterval')).to.be.an('object'); - expect(state.get('time')).to.be.an('object'); - - state.removeProps(['refreshInterval', 'time']); - expect(state.get('refreshInterval')).to.be(undefined); - expect(state.get('time')).to.be(undefined); - }); - }); - - describe('toString', function () { - it('should rison encode the state', function () { - const state = parseKibanaState(query, 'global'); - expect(state.toString()).to.equal(globalTime); - }); - }); - - describe('toQuery', function () { - it('should return an object', function () { - const state = parseKibanaState(query, 'global'); - expect(state.toQuery()).to.be.an('object'); - }); - - it('should contain the kibana state property', function () { - const state = parseKibanaState(query, 'global'); - expect(state.toQuery()).to.have.property(stateIndices.global, globalTime); - }); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/check_license/check_license.js b/x-pack/legacy/server/lib/check_license/check_license.js deleted file mode 100644 index 7695755622310..0000000000000 --- a/x-pack/legacy/server/lib/check_license/check_license.js +++ /dev/null @@ -1,75 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { - LICENSE_STATUS_UNAVAILABLE, - LICENSE_STATUS_INVALID, - LICENSE_STATUS_EXPIRED, - LICENSE_STATUS_VALID, - RANKED_LICENSE_TYPES, -} from '../../../common/constants'; - -export function checkLicense(pluginName, minimumLicenseRequired, xpackLicenseInfo) { - if (!minimumLicenseRequired) { - throw new Error( - `Error checking license for plugin "${pluginName}". The minimum license required has not been provided.` - ); - } - - if (!RANKED_LICENSE_TYPES.includes(minimumLicenseRequired)) { - throw new Error(`Invalid license type supplied to checkLicense: ${minimumLicenseRequired}`); - } - - // If, for some reason, we cannot get the license information - // from Elasticsearch, assume worst case and disable - if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) { - return { - status: LICENSE_STATUS_UNAVAILABLE, - message: i18n.translate('xpack.server.checkLicense.errorUnavailableMessage', { - defaultMessage: - 'You cannot use {pluginName} because license information is not available at this time.', - values: { pluginName }, - }), - }; - } - - const { license } = xpackLicenseInfo; - const isLicenseModeValid = license.isOneOf( - [...RANKED_LICENSE_TYPES].splice(RANKED_LICENSE_TYPES.indexOf(minimumLicenseRequired)) - ); - const isLicenseActive = license.isActive(); - const licenseType = license.getType(); - - // License is not valid - if (!isLicenseModeValid) { - return { - status: LICENSE_STATUS_INVALID, - message: i18n.translate('xpack.server.checkLicense.errorUnsupportedMessage', { - defaultMessage: - 'Your {licenseType} license does not support {pluginName}. Please upgrade your license.', - values: { licenseType, pluginName }, - }), - }; - } - - // License is valid but not active - if (!isLicenseActive) { - return { - status: LICENSE_STATUS_EXPIRED, - message: i18n.translate('xpack.server.checkLicense.errorExpiredMessage', { - defaultMessage: - 'You cannot use {pluginName} because your {licenseType} license has expired.', - values: { licenseType, pluginName }, - }), - }; - } - - // License is valid and active - return { - status: LICENSE_STATUS_VALID, - }; -} diff --git a/x-pack/legacy/server/lib/check_license/check_license.test.js b/x-pack/legacy/server/lib/check_license/check_license.test.js deleted file mode 100644 index 65b599ed4a5f6..0000000000000 --- a/x-pack/legacy/server/lib/check_license/check_license.test.js +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { set } from '@elastic/safer-lodash-set'; -import { checkLicense } from './check_license'; -import { - LICENSE_STATUS_UNAVAILABLE, - LICENSE_STATUS_EXPIRED, - LICENSE_STATUS_VALID, - LICENSE_TYPE_BASIC, -} from '../../../common/constants'; - -describe('check_license', function () { - const pluginName = 'Foo'; - const minimumLicenseRequired = LICENSE_TYPE_BASIC; - let mockLicenseInfo; - beforeEach(() => (mockLicenseInfo = {})); - - describe('license information is undefined', () => { - beforeEach(() => (mockLicenseInfo = undefined)); - - it('should set status to unavailable', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).status).toBe( - LICENSE_STATUS_UNAVAILABLE - ); - }); - - it('should set a message', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).message).not.toBe( - undefined - ); - }); - }); - - describe('license information is not available', () => { - beforeEach(() => (mockLicenseInfo.isAvailable = () => false)); - - it('should set status to unavailable', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).status).toBe( - LICENSE_STATUS_UNAVAILABLE - ); - }); - - it('should set a message', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).message).not.toBe( - undefined - ); - }); - }); - - describe('license information is available', () => { - beforeEach(() => { - mockLicenseInfo.isAvailable = () => true; - set(mockLicenseInfo, 'license.getType', () => LICENSE_TYPE_BASIC); - }); - - describe('& license is trial, standard, gold, platinum', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true)); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set status to valid', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).status).toBe( - LICENSE_STATUS_VALID - ); - }); - - it('should not set a message', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).message).toBe( - undefined - ); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set status to inactive', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).status).toBe( - LICENSE_STATUS_EXPIRED - ); - }); - - it('should set a message', () => { - expect( - checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).message - ).not.toBe(undefined); - }); - }); - }); - - describe('& license is basic', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true)); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set status to valid', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).status).toBe( - LICENSE_STATUS_VALID - ); - }); - - it('should not set a message', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).message).toBe( - undefined - ); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set status to inactive', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).status).toBe( - LICENSE_STATUS_EXPIRED - ); - }); - - it('should set a message', () => { - expect( - checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).message - ).not.toBe(undefined); - }); - }); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/check_license/index.js b/x-pack/legacy/server/lib/check_license/index.js deleted file mode 100644 index f2c070fd44b6e..0000000000000 --- a/x-pack/legacy/server/lib/check_license/index.js +++ /dev/null @@ -1,7 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export { checkLicense } from './check_license'; diff --git a/x-pack/legacy/server/lib/constants/index.ts b/x-pack/legacy/server/lib/constants/index.ts deleted file mode 100644 index 2378aca824042..0000000000000 --- a/x-pack/legacy/server/lib/constants/index.ts +++ /dev/null @@ -1,7 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export { XPACK_INFO_API_DEFAULT_POLL_FREQUENCY_IN_MILLIS } from './xpack_info'; diff --git a/x-pack/legacy/server/lib/constants/xpack_info.ts b/x-pack/legacy/server/lib/constants/xpack_info.ts deleted file mode 100644 index c58bb275245b6..0000000000000 --- a/x-pack/legacy/server/lib/constants/xpack_info.ts +++ /dev/null @@ -1,7 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export const XPACK_INFO_API_DEFAULT_POLL_FREQUENCY_IN_MILLIS = 30001; // 30 seconds diff --git a/x-pack/legacy/server/lib/create_router/call_with_request_factory/call_with_request_factory.js b/x-pack/legacy/server/lib/create_router/call_with_request_factory/call_with_request_factory.js deleted file mode 100644 index df1ce95b31655..0000000000000 --- a/x-pack/legacy/server/lib/create_router/call_with_request_factory/call_with_request_factory.js +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -export const callWithRequestFactory = (server, pluginId, config) => { - const { callWithRequest } = config - ? server.plugins.elasticsearch.createCluster(pluginId, config) - : server.plugins.elasticsearch.getCluster('data'); - return callWithRequest; -}; diff --git a/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts b/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts deleted file mode 100644 index 3537d1bf42079..0000000000000 --- a/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Legacy } from 'kibana'; -import { LegacyAPICaller } from '../../../../../../src/core/server'; - -export type CallWithRequest = (...args: any[]) => LegacyAPICaller; - -export declare function callWithRequestFactory( - server: Legacy.Server, - pluginId: string, - config?: { - plugins: any[]; - } -): CallWithRequest; diff --git a/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.js b/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.js deleted file mode 100644 index 787814d87dff9..0000000000000 --- a/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.js +++ /dev/null @@ -1,7 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export { callWithRequestFactory } from './call_with_request_factory'; diff --git a/x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_custom_error.js b/x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_custom_error.js deleted file mode 100644 index f9c102be7a1ff..0000000000000 --- a/x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_custom_error.js +++ /dev/null @@ -1,21 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { wrapCustomError } from '../wrap_custom_error'; - -describe('wrap_custom_error', () => { - describe('#wrapCustomError', () => { - it('should return a Boom object', () => { - const originalError = new Error('I am an error'); - const statusCode = 404; - const wrappedError = wrapCustomError(originalError, statusCode); - - expect(wrappedError.isBoom).to.be(true); - expect(wrappedError.output.statusCode).to.equal(statusCode); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_es_error.js b/x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_es_error.js deleted file mode 100644 index 8241dc4329137..0000000000000 --- a/x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_es_error.js +++ /dev/null @@ -1,39 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { wrapEsError } from '../wrap_es_error'; - -describe('wrap_es_error', () => { - describe('#wrapEsError', () => { - let originalError; - beforeEach(() => { - originalError = new Error('I am an error'); - originalError.statusCode = 404; - originalError.response = '{}'; - }); - - it('should return a Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.isBoom).to.be(true); - }); - - it('should return the correct Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be(originalError.message); - }); - - it('should return the correct Boom object with custom message', () => { - const wrappedError = wrapEsError(originalError, { 404: 'No encontrado!' }); - - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be('No encontrado!'); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_unknown_error.js b/x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_unknown_error.js deleted file mode 100644 index 85e0b2b3033ad..0000000000000 --- a/x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_unknown_error.js +++ /dev/null @@ -1,19 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { wrapUnknownError } from '../wrap_unknown_error'; - -describe('wrap_unknown_error', () => { - describe('#wrapUnknownError', () => { - it('should return a Boom object', () => { - const originalError = new Error('I am an error'); - const wrappedError = wrapUnknownError(originalError); - - expect(wrappedError.isBoom).to.be(true); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/create_router/error_wrappers/index.d.ts b/x-pack/legacy/server/lib/create_router/error_wrappers/index.d.ts deleted file mode 100644 index 1aaefb4e3727c..0000000000000 --- a/x-pack/legacy/server/lib/create_router/error_wrappers/index.d.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; - * you may not use this file except in compliance with the Elastic License. - */ -import Boom from 'boom'; - -export declare function wrapCustomError(error: Error, statusCode: number): Boom; - -export declare function wrapEsError(error: Error, statusCodeToMessageMap?: object): Boom; - -export declare function wrapUnknownError(error: Error): Boom; diff --git a/x-pack/legacy/server/lib/create_router/error_wrappers/index.js b/x-pack/legacy/server/lib/create_router/error_wrappers/index.js deleted file mode 100644 index f275f15637091..0000000000000 --- a/x-pack/legacy/server/lib/create_router/error_wrappers/index.js +++ /dev/null @@ -1,9 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export { wrapCustomError } from './wrap_custom_error'; -export { wrapEsError } from './wrap_es_error'; -export { wrapUnknownError } from './wrap_unknown_error'; diff --git a/x-pack/legacy/server/lib/create_router/error_wrappers/wrap_custom_error.js b/x-pack/legacy/server/lib/create_router/error_wrappers/wrap_custom_error.js deleted file mode 100644 index 3295113d38ee5..0000000000000 --- a/x-pack/legacy/server/lib/create_router/error_wrappers/wrap_custom_error.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -/** - * Wraps a custom error into a Boom error response and returns it - * - * @param err Object error - * @param statusCode Error status code - * @return Object Boom error response - */ -export function wrapCustomError(err, statusCode) { - return Boom.boomify(err, { statusCode }); -} diff --git a/x-pack/legacy/server/lib/create_router/error_wrappers/wrap_es_error.js b/x-pack/legacy/server/lib/create_router/error_wrappers/wrap_es_error.js deleted file mode 100644 index 72be6321af8a2..0000000000000 --- a/x-pack/legacy/server/lib/create_router/error_wrappers/wrap_es_error.js +++ /dev/null @@ -1,59 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -function extractCausedByChain(causedBy = {}, accumulator = []) { - const { reason, caused_by } = causedBy; // eslint-disable-line camelcase - - if (reason) { - accumulator.push(reason); - } - - // eslint-disable-next-line camelcase - if (caused_by) { - return extractCausedByChain(caused_by, accumulator); - } - - return accumulator; -} - -/** - * Wraps an error thrown by the ES JS client into a Boom error response and returns it - * - * @param err Object Error thrown by ES JS client - * @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages - * @return Object Boom error response - */ -export function wrapEsError(err, statusCodeToMessageMap = {}) { - const { statusCode, response } = err; - - const { - error: { - root_cause = [], // eslint-disable-line camelcase - caused_by, // eslint-disable-line camelcase - } = {}, - } = JSON.parse(response); - - // If no custom message if specified for the error's status code, just - // wrap the error as a Boom error response, include the additional information from ES, and return it - if (!statusCodeToMessageMap[statusCode]) { - const boomError = Boom.boomify(err, { statusCode }); - - // The caused_by chain has the most information so use that if it's available. If not then - // settle for the root_cause. - const causedByChain = extractCausedByChain(caused_by); - const defaultCause = root_cause.length ? extractCausedByChain(root_cause[0]) : undefined; - - boomError.output.payload.cause = causedByChain.length ? causedByChain : defaultCause; - return boomError; - } - - // Otherwise, use the custom message to create a Boom error response and - // return it - const message = statusCodeToMessageMap[statusCode]; - return new Boom(message, { statusCode }); -} diff --git a/x-pack/legacy/server/lib/create_router/error_wrappers/wrap_unknown_error.js b/x-pack/legacy/server/lib/create_router/error_wrappers/wrap_unknown_error.js deleted file mode 100644 index ffd915c513362..0000000000000 --- a/x-pack/legacy/server/lib/create_router/error_wrappers/wrap_unknown_error.js +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -/** - * Wraps an unknown error into a Boom error response and returns it - * - * @param err Object Unknown error - * @return Object Boom error response - */ -export function wrapUnknownError(err) { - return Boom.boomify(err); -} diff --git a/x-pack/legacy/server/lib/create_router/index.d.ts b/x-pack/legacy/server/lib/create_router/index.d.ts deleted file mode 100644 index 76e5f4b599708..0000000000000 --- a/x-pack/legacy/server/lib/create_router/index.d.ts +++ /dev/null @@ -1,38 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import { Request, ResponseToolkit } from 'hapi'; -import { Legacy } from 'kibana'; -import { CallWithRequest } from './call_with_request_factory'; - -export * from './error_wrappers'; - -export type RouterRouteHandler = ( - req: Request, - callWithRequest: ReturnType, - responseToolkit: ResponseToolkit -) => Promise; - -export type RouterRoute = (path: string, handler: RouterRouteHandler) => Router; - -export interface Router { - get: RouterRoute; - post: RouterRoute; - put: RouterRoute; - delete: RouterRoute; - patch: RouterRoute; - isEsError: any; -} - -export declare function createRouter( - server: Legacy.Server, - pluginId: string, - apiBasePath: string, - config?: { - plugins: any[]; - } -): Router; - -export declare function isEsErrorFactory(server: Legacy.Server): any; diff --git a/x-pack/legacy/server/lib/create_router/index.js b/x-pack/legacy/server/lib/create_router/index.js deleted file mode 100644 index e4d66bdb5a48b..0000000000000 --- a/x-pack/legacy/server/lib/create_router/index.js +++ /dev/null @@ -1,61 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import Boom from 'boom'; -import { callWithRequestFactory } from './call_with_request_factory'; -import { isEsErrorFactory as createIsEsError } from './is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from './error_wrappers'; -import { licensePreRoutingFactory } from './license_pre_routing_factory'; - -export { wrapEsError, wrapUnknownError, wrapCustomError } from './error_wrappers'; - -// Sometimes consumers will need to check if errors are ES errors, too. -export const isEsErrorFactory = (server) => { - return createIsEsError(server); -}; - -export const createRouter = (server, pluginId, apiBasePath = '', config) => { - const isEsError = isEsErrorFactory(server); - - // NOTE: The license-checking logic depends on the xpack_main plugin, so if your plugin - // consumes this helper, make sure it declares 'xpack_main' as a dependency. - const licensePreRouting = licensePreRoutingFactory(server, pluginId); - - const callWithRequestInstance = callWithRequestFactory(server, pluginId, config); - - const requestHandler = (handler) => async (request, h) => { - try { - const callWithRequest = (...args) => { - return callWithRequestInstance(request, ...args); - }; - return await handler(request, callWithRequest, h); - } catch (err) { - if (err instanceof Boom) { - throw err; - } - - if (isEsError(err)) { - throw wrapEsError(err); - } - - throw wrapUnknownError(err); - } - }; - - // Decorate base router with HTTP methods. - return ['get', 'post', 'put', 'delete', 'patch'].reduce((router, methodName) => { - router[methodName] = (subPath, handler) => { - const method = methodName.toUpperCase(); - const path = apiBasePath + subPath; - server.route({ - path, - method, - handler: requestHandler(handler), - config: { pre: [licensePreRouting] }, - }); - }; - return router; - }, {}); -}; diff --git a/x-pack/legacy/server/lib/create_router/is_es_error_factory/__tests__/is_es_error_factory.js b/x-pack/legacy/server/lib/create_router/is_es_error_factory/__tests__/is_es_error_factory.js deleted file mode 100644 index ef6fbaf9c53d0..0000000000000 --- a/x-pack/legacy/server/lib/create_router/is_es_error_factory/__tests__/is_es_error_factory.js +++ /dev/null @@ -1,44 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { isEsErrorFactory } from '../is_es_error_factory'; -import { set } from '@elastic/safer-lodash-set'; - -class MockAbstractEsError {} - -describe('is_es_error_factory', () => { - let mockServer; - let isEsError; - - beforeEach(() => { - const mockEsErrors = { - _Abstract: MockAbstractEsError, - }; - mockServer = {}; - set(mockServer, 'plugins.elasticsearch.getCluster', () => ({ errors: mockEsErrors })); - - isEsError = isEsErrorFactory(mockServer); - }); - - describe('#isEsErrorFactory', () => { - it('should return a function', () => { - expect(isEsError).to.be.a(Function); - }); - - describe('returned function', () => { - it('should return true if passed-in err is a known esError', () => { - const knownEsError = new MockAbstractEsError(); - expect(isEsError(knownEsError)).to.be(true); - }); - - it('should return false if passed-in err is not a known esError', () => { - const unknownEsError = {}; - expect(isEsError(unknownEsError)).to.be(false); - }); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/create_router/is_es_error_factory/index.js b/x-pack/legacy/server/lib/create_router/is_es_error_factory/index.js deleted file mode 100644 index 441648a8701e0..0000000000000 --- a/x-pack/legacy/server/lib/create_router/is_es_error_factory/index.js +++ /dev/null @@ -1,7 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export { isEsErrorFactory } from './is_es_error_factory'; diff --git a/x-pack/legacy/server/lib/create_router/is_es_error_factory/is_es_error_factory.js b/x-pack/legacy/server/lib/create_router/is_es_error_factory/is_es_error_factory.js deleted file mode 100644 index 80daac5bd496d..0000000000000 --- a/x-pack/legacy/server/lib/create_router/is_es_error_factory/is_es_error_factory.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { memoize } from 'lodash'; - -const esErrorsFactory = memoize((server) => { - return server.plugins.elasticsearch.getCluster('admin').errors; -}); - -export function isEsErrorFactory(server) { - const esErrors = esErrorsFactory(server); - return function isEsError(err) { - return err instanceof esErrors._Abstract; - }; -} diff --git a/x-pack/legacy/server/lib/create_router/license_pre_routing_factory/__tests__/license_pre_routing_factory.js b/x-pack/legacy/server/lib/create_router/license_pre_routing_factory/__tests__/license_pre_routing_factory.js deleted file mode 100644 index dde18a0ccd7dd..0000000000000 --- a/x-pack/legacy/server/lib/create_router/license_pre_routing_factory/__tests__/license_pre_routing_factory.js +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { licensePreRoutingFactory } from '../license_pre_routing_factory'; -import { LICENSE_STATUS_INVALID, LICENSE_STATUS_VALID } from '../../../../../common/constants'; - -describe('license_pre_routing_factory', () => { - describe('#reportingFeaturePreRoutingFactory', () => { - let mockServer; - let mockLicenseCheckResults; - - beforeEach(() => { - mockServer = { - plugins: { - xpack_main: { - info: { - feature: () => ({ - getLicenseCheckResults: () => mockLicenseCheckResults, - }), - }, - }, - }, - }; - }); - - it('instantiates a new instance per plugin', () => { - const firstInstance = licensePreRoutingFactory(mockServer, 'foo'); - const secondInstance = licensePreRoutingFactory(mockServer, 'bar'); - - expect(firstInstance).to.not.be(secondInstance); - }); - - describe('status is invalid', () => { - beforeEach(() => { - mockLicenseCheckResults = { - status: LICENSE_STATUS_INVALID, - }; - }); - - it('replies with 403', () => { - const licensePreRouting = licensePreRoutingFactory(mockServer); - const stubRequest = {}; - expect(() => licensePreRouting(stubRequest)).to.throwException((response) => { - expect(response).to.be.an(Error); - expect(response.isBoom).to.be(true); - expect(response.output.statusCode).to.be(403); - }); - }); - }); - - describe('status is valid', () => { - beforeEach(() => { - mockLicenseCheckResults = { - status: LICENSE_STATUS_VALID, - }; - }); - - it('replies with nothing', () => { - const licensePreRouting = licensePreRoutingFactory(mockServer); - const stubRequest = {}; - const response = licensePreRouting(stubRequest); - expect(response).to.be(null); - }); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/create_router/license_pre_routing_factory/index.js b/x-pack/legacy/server/lib/create_router/license_pre_routing_factory/index.js deleted file mode 100644 index 0743e443955f4..0000000000000 --- a/x-pack/legacy/server/lib/create_router/license_pre_routing_factory/index.js +++ /dev/null @@ -1,7 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export { licensePreRoutingFactory } from './license_pre_routing_factory'; diff --git a/x-pack/legacy/server/lib/create_router/license_pre_routing_factory/license_pre_routing_factory.js b/x-pack/legacy/server/lib/create_router/license_pre_routing_factory/license_pre_routing_factory.js deleted file mode 100644 index 81640ebb35ea9..0000000000000 --- a/x-pack/legacy/server/lib/create_router/license_pre_routing_factory/license_pre_routing_factory.js +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { wrapCustomError } from '../error_wrappers'; -import { LICENSE_STATUS_VALID } from '../../../../common/constants'; - -export const licensePreRoutingFactory = (server, pluginId) => { - return () => { - const xpackMainPlugin = server.plugins.xpack_main; - const licenseCheckResults = xpackMainPlugin.info.feature(pluginId).getLicenseCheckResults(); - - // Apps which don't have any license restrictions will return undefined license check results. - if (licenseCheckResults) { - if (licenseCheckResults.status !== LICENSE_STATUS_VALID) { - const error = new Error(licenseCheckResults.message); - const statusCode = 403; - throw wrapCustomError(error, statusCode); - } - } - - return null; - }; -}; diff --git a/x-pack/legacy/server/lib/key_case_converter.js b/x-pack/legacy/server/lib/key_case_converter.js deleted file mode 100644 index a2a5452b3a1d9..0000000000000 --- a/x-pack/legacy/server/lib/key_case_converter.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import _ from 'lodash'; - -// Note: This function uses _.clone. This will clone objects created by constructors other than Object -// to plain Object objects. Uncloneable values such as functions, DOM nodes, Maps, Sets, and WeakMaps -// will be cloned to the empty object. -function convertKeysToSpecifiedCaseDeep(object, caseConversionFunction) { - // Base case - if (!(_.isPlainObject(object) || _.isArray(object))) { - return object; - } - - // Clone (so we don't modify the original object that was passed in) - let newObject; - if (Array.isArray(object)) { - newObject = object.slice(0); - } else { - newObject = _.clone(object); - - // Convert top-level keys - newObject = _.mapKeys(newObject, (value, key) => caseConversionFunction(key)); - } - - // Recursively convert nested object keys - _.forEach( - newObject, - (value, key) => (newObject[key] = convertKeysToSpecifiedCaseDeep(value, caseConversionFunction)) - ); - - return newObject; -} - -function validateObject(object) { - if (!(_.isPlainObject(object) || _.isArray(object))) { - throw new Error('Specified object should be an Object or Array'); - } -} - -export function convertKeysToSnakeCaseDeep(object) { - validateObject(object); - return convertKeysToSpecifiedCaseDeep(object, _.snakeCase); -} - -export function convertKeysToCamelCaseDeep(object) { - validateObject(object); - return convertKeysToSpecifiedCaseDeep(object, _.camelCase); -} diff --git a/x-pack/legacy/server/lib/parse_kibana_state.js b/x-pack/legacy/server/lib/parse_kibana_state.js deleted file mode 100644 index a6c9bfbb511c1..0000000000000 --- a/x-pack/legacy/server/lib/parse_kibana_state.js +++ /dev/null @@ -1,55 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { set } from '@elastic/safer-lodash-set'; -import { isPlainObject, omit, get } from 'lodash'; -import rison from 'rison-node'; - -const stateTypeKeys = { - global: '_g', - app: '_a', -}; - -class KibanaState { - constructor(query, type = 'global') { - const propId = stateTypeKeys[type]; - if (!isPlainObject(query)) throw new TypeError('Query parameter must be an object'); - if (!propId) throw new TypeError(`Unknown state type: '${type}'`); - - const queryValue = query[propId]; - - this.exists = Boolean(queryValue); - this.state = queryValue ? rison.decode(queryValue) : {}; - this.type = type; - } - - removeProps(props) { - this.state = omit(this.state, props); - } - - get(prop, defVal) { - return get(this.state, prop, defVal); - } - - set(prop, val) { - return set(this.state, prop, val); - } - - toString() { - return rison.encode(this.state); - } - - toQuery() { - const index = stateTypeKeys[this.type]; - const output = {}; - output[index] = this.toString(); - return output; - } -} - -export function parseKibanaState(query, type) { - return new KibanaState(query, type); -} diff --git a/x-pack/legacy/server/lib/register_license_checker/index.d.ts b/x-pack/legacy/server/lib/register_license_checker/index.d.ts deleted file mode 100644 index 555008921df42..0000000000000 --- a/x-pack/legacy/server/lib/register_license_checker/index.d.ts +++ /dev/null @@ -1,15 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Legacy } from 'kibana'; -import { LicenseType } from '../../../common/constants'; - -export declare function registerLicenseChecker( - server: Legacy.Server, - pluginId: string, - pluginName: string, - minimumLicenseRequired: LicenseType -): void; diff --git a/x-pack/legacy/server/lib/register_license_checker/index.js b/x-pack/legacy/server/lib/register_license_checker/index.js deleted file mode 100644 index 7b0f97c38d129..0000000000000 --- a/x-pack/legacy/server/lib/register_license_checker/index.js +++ /dev/null @@ -1,7 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export { registerLicenseChecker } from './register_license_checker'; diff --git a/x-pack/legacy/server/lib/register_license_checker/register_license_checker.js b/x-pack/legacy/server/lib/register_license_checker/register_license_checker.js deleted file mode 100644 index 57cbe30c25cb2..0000000000000 --- a/x-pack/legacy/server/lib/register_license_checker/register_license_checker.js +++ /dev/null @@ -1,34 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { pairwise } from 'rxjs/operators'; - -import { ServiceStatusLevels } from '../../../../../src/core/server'; -import { checkLicense } from '../check_license'; - -export function registerLicenseChecker(server, pluginId, pluginName, minimumLicenseRequired) { - const xpackMainPlugin = server.plugins.xpack_main; - const subscription = server.newPlatform.setup.core.status.core$ - .pipe(pairwise()) - .subscribe(([coreLast, coreCurrent]) => { - if ( - !subscription.closed && - coreLast.elasticsearch.level !== ServiceStatusLevels.available && - coreCurrent.elasticsearch.level === ServiceStatusLevels.available - ) { - // Unsubscribe as soon as ES becomes available so this function only runs once - subscription.unsubscribe(); - - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info - .feature(pluginId) - .registerLicenseCheckResultsGenerator((xpackLicenseInfo) => { - return checkLicense(pluginName, minimumLicenseRequired, xpackLicenseInfo); - }); - } - }); -} diff --git a/x-pack/legacy/server/lib/watch_status_and_license_to_initialize.js b/x-pack/legacy/server/lib/watch_status_and_license_to_initialize.js deleted file mode 100644 index 109dbbb20e35d..0000000000000 --- a/x-pack/legacy/server/lib/watch_status_and_license_to_initialize.js +++ /dev/null @@ -1,83 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import * as Rx from 'rxjs'; -import { catchError, mergeMap, map, switchMap, tap } from 'rxjs/operators'; - -export const RETRY_SCALE_DURATION = 100; -export const RETRY_DURATION_MAX = 10000; - -const calculateDuration = (i) => { - const duration = i * RETRY_SCALE_DURATION; - if (duration > RETRY_DURATION_MAX) { - return RETRY_DURATION_MAX; - } - - return duration; -}; - -// we can't use a retryWhen here, because we want to propagate the red status and then retry -const propagateRedStatusAndScaleRetry = () => { - let i = 0; - return (err, caught) => - Rx.concat( - Rx.of({ - state: 'red', - message: err.message, - }), - Rx.timer(calculateDuration(++i)).pipe(mergeMap(() => caught)) - ); -}; - -export function watchStatusAndLicenseToInitialize(xpackMainPlugin, downstreamPlugin, initialize) { - const xpackInfo = xpackMainPlugin.info; - const xpackInfoFeature = xpackInfo.feature(downstreamPlugin.id); - - const upstreamStatus = xpackMainPlugin.status; - const currentStatus$ = Rx.of({ - state: upstreamStatus.state, - message: upstreamStatus.message, - }); - const newStatus$ = Rx.fromEvent( - upstreamStatus, - 'change', - null, - (previousState, previousMsg, state, message) => { - return { - state, - message, - }; - } - ); - const status$ = Rx.merge(currentStatus$, newStatus$); - - const currentLicense$ = Rx.of(xpackInfoFeature.getLicenseCheckResults()); - const newLicense$ = Rx.fromEventPattern(xpackInfo.onLicenseInfoChange.bind(xpackInfo)).pipe( - map(() => xpackInfoFeature.getLicenseCheckResults()) - ); - const license$ = Rx.merge(currentLicense$, newLicense$); - - Rx.combineLatest(status$, license$) - .pipe( - map(([status, license]) => ({ status, license })), - switchMap(({ status, license }) => { - if (status.state !== 'green') { - return Rx.of({ state: status.state, message: status.message }); - } - - return Rx.defer(() => initialize(license)).pipe( - map(() => ({ - state: 'green', - message: 'Ready', - })), - catchError(propagateRedStatusAndScaleRetry()) - ); - }), - tap(({ state, message }) => { - downstreamPlugin.status[state](message); - }) - ) - .subscribe(); -} diff --git a/x-pack/legacy/server/lib/watch_status_and_license_to_initialize.test.js b/x-pack/legacy/server/lib/watch_status_and_license_to_initialize.test.js deleted file mode 100644 index 33282b7591db7..0000000000000 --- a/x-pack/legacy/server/lib/watch_status_and_license_to_initialize.test.js +++ /dev/null @@ -1,301 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EventEmitter } from 'events'; -import { - watchStatusAndLicenseToInitialize, - RETRY_SCALE_DURATION, - RETRY_DURATION_MAX, -} from './watch_status_and_license_to_initialize'; - -const createMockXpackMainPluginAndFeature = (featureId) => { - const licenseChangeCallbacks = []; - - const mockFeature = { - getLicenseCheckResults: jest.fn(), - mock: { - triggerLicenseChange: () => { - for (const callback of licenseChangeCallbacks) { - callback(); - } - }, - setLicenseCheckResults: (value) => { - mockFeature.getLicenseCheckResults.mockReturnValue(value); - }, - }, - }; - - const mockXpackMainPlugin = { - info: { - onLicenseInfoChange: (callback) => { - licenseChangeCallbacks.push(callback); - }, - feature: (id) => { - if (id === featureId) { - return mockFeature; - } - throw new Error('Unexpected feature'); - }, - }, - status: new EventEmitter(), - mock: { - setStatus: (state, message) => { - mockXpackMainPlugin.status.state = state; - mockXpackMainPlugin.status.message = message; - mockXpackMainPlugin.status.emit('change', null, null, state, message); - }, - }, - }; - - return { mockXpackMainPlugin, mockFeature }; -}; - -const createMockDownstreamPlugin = (id) => { - const defaultImplementation = () => { - throw new Error('Not implemented'); - }; - return { - id, - status: { - disabled: jest.fn().mockImplementation(defaultImplementation), - yellow: jest.fn().mockImplementation(defaultImplementation), - green: jest.fn().mockImplementation(defaultImplementation), - red: jest.fn().mockImplementation(defaultImplementation), - }, - }; -}; - -const advanceRetry = async (initializeCount) => { - await Promise.resolve(); - let duration = initializeCount * RETRY_SCALE_DURATION; - if (duration > RETRY_DURATION_MAX) { - duration = RETRY_DURATION_MAX; - } - jest.advanceTimersByTime(duration); -}; - -['red', 'yellow', 'disabled'].forEach((state) => { - test(`mirrors ${state} immediately`, () => { - const pluginId = 'foo-plugin'; - const message = `${state} is now the state`; - const { mockXpackMainPlugin } = createMockXpackMainPluginAndFeature(pluginId); - mockXpackMainPlugin.mock.setStatus(state, message); - const downstreamPlugin = createMockDownstreamPlugin(pluginId); - const initializeMock = jest.fn(); - downstreamPlugin.status[state].mockImplementation(() => {}); - - watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); - - expect(initializeMock).not.toHaveBeenCalled(); - expect(downstreamPlugin.status[state]).toHaveBeenCalledTimes(1); - expect(downstreamPlugin.status[state]).toHaveBeenCalledWith(message); - }); -}); - -test(`calls initialize and doesn't immediately set downstream status when the initial status is green`, () => { - const pluginId = 'foo-plugin'; - const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); - mockXpackMainPlugin.mock.setStatus('green', 'green is now the state'); - const licenseCheckResults = Symbol(); - mockFeature.mock.setLicenseCheckResults(licenseCheckResults); - const downstreamPlugin = createMockDownstreamPlugin(pluginId); - const initializeMock = jest.fn().mockImplementation(() => new Promise(() => {})); - - watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); - - expect(initializeMock).toHaveBeenCalledTimes(1); - expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults); - expect(downstreamPlugin.status.green).toHaveBeenCalledTimes(0); -}); - -test(`sets downstream plugin's status to green when initialize resolves`, (done) => { - const pluginId = 'foo-plugin'; - const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); - mockXpackMainPlugin.mock.setStatus('green', 'green is now the state'); - const licenseCheckResults = Symbol(); - mockFeature.mock.setLicenseCheckResults(licenseCheckResults); - const downstreamPlugin = createMockDownstreamPlugin(pluginId); - const initializeMock = jest.fn().mockImplementation(() => Promise.resolve()); - - watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); - - expect(initializeMock).toHaveBeenCalledTimes(1); - expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults); - downstreamPlugin.status.green.mockImplementation((actualMessage) => { - expect(actualMessage).toBe('Ready'); - done(); - }); -}); - -test(`sets downstream plugin's status to red when initialize initially rejects, and continually polls initialize`, (done) => { - jest.useFakeTimers(); - - const pluginId = 'foo-plugin'; - const errorMessage = 'the error message'; - const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); - mockXpackMainPlugin.mock.setStatus('green'); - const licenseCheckResults = Symbol(); - mockFeature.mock.setLicenseCheckResults(licenseCheckResults); - const downstreamPlugin = createMockDownstreamPlugin(pluginId); - - let isRed = false; - let initializeCount = 0; - const initializeMock = jest.fn().mockImplementation(() => { - ++initializeCount; - - // on the second retry, ensure we already set the status to red - if (initializeCount === 2) { - expect(isRed).toBe(true); - } - - // this should theoretically continue indefinitely, but we only have so long to run the tests - if (initializeCount === 100) { - done(); - } - - // everytime this is called, we have to wait for a new promise to be resolved - // allowing the Promise the we return below to run, and then advance the timers - setImmediate(() => { - advanceRetry(initializeCount); - }); - return Promise.reject(new Error(errorMessage)); - }); - - watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); - - expect(initializeMock).toHaveBeenCalledTimes(1); - expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults); - downstreamPlugin.status.red.mockImplementation((message) => { - isRed = true; - expect(message).toBe(errorMessage); - }); -}); - -test(`sets downstream plugin's status to green when initialize resolves after rejecting 10 times`, (done) => { - jest.useFakeTimers(); - - const pluginId = 'foo-plugin'; - const errorMessage = 'the error message'; - const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); - mockXpackMainPlugin.mock.setStatus('green'); - const licenseCheckResults = Symbol(); - mockFeature.mock.setLicenseCheckResults(licenseCheckResults); - const downstreamPlugin = createMockDownstreamPlugin(pluginId); - - let initializeCount = 0; - const initializeMock = jest.fn().mockImplementation(() => { - ++initializeCount; - - // everytime this is called, we have to wait for a new promise to be resolved - // allowing the Promise the we return below to run, and then advance the timers - setImmediate(() => { - advanceRetry(initializeCount); - }); - - if (initializeCount >= 10) { - return Promise.resolve(); - } - - return Promise.reject(new Error(errorMessage)); - }); - - watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); - - expect(initializeMock).toHaveBeenCalledTimes(1); - expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults); - downstreamPlugin.status.red.mockImplementation((message) => { - expect(initializeCount).toBeLessThan(10); - expect(message).toBe(errorMessage); - }); - downstreamPlugin.status.green.mockImplementation((message) => { - expect(initializeCount).toBe(10); - expect(message).toBe('Ready'); - done(); - }); -}); - -test(`calls initialize twice when it gets a new license and the status is green`, (done) => { - const pluginId = 'foo-plugin'; - const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); - mockXpackMainPlugin.mock.setStatus('green'); - const firstLicenseCheckResults = Symbol(); - const secondLicenseCheckResults = Symbol(); - mockFeature.mock.setLicenseCheckResults(firstLicenseCheckResults); - const downstreamPlugin = createMockDownstreamPlugin(pluginId); - const initializeMock = jest.fn().mockImplementation(() => Promise.resolve()); - - let count = 0; - downstreamPlugin.status.green.mockImplementation((message) => { - expect(message).toBe('Ready'); - ++count; - if (count === 1) { - mockFeature.mock.setLicenseCheckResults(secondLicenseCheckResults); - mockFeature.mock.triggerLicenseChange(); - } - if (count === 2) { - expect(initializeMock).toHaveBeenCalledWith(firstLicenseCheckResults); - expect(initializeMock).toHaveBeenCalledWith(secondLicenseCheckResults); - expect(initializeMock).toHaveBeenCalledTimes(2); - done(); - } - }); - - watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); -}); - -test(`doesn't call initialize twice when it gets a new license when the status isn't green`, (done) => { - const pluginId = 'foo-plugin'; - const redMessage = 'the red message'; - const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); - mockXpackMainPlugin.mock.setStatus('green'); - const firstLicenseCheckResults = Symbol(); - const secondLicenseCheckResults = Symbol(); - mockFeature.mock.setLicenseCheckResults(firstLicenseCheckResults); - const downstreamPlugin = createMockDownstreamPlugin(pluginId); - const initializeMock = jest.fn().mockImplementation(() => Promise.resolve()); - - downstreamPlugin.status.green.mockImplementation((message) => { - expect(message).toBe('Ready'); - mockXpackMainPlugin.mock.setStatus('red', redMessage); - mockFeature.mock.setLicenseCheckResults(secondLicenseCheckResults); - mockFeature.mock.triggerLicenseChange(); - }); - - downstreamPlugin.status.red.mockImplementation((message) => { - expect(message).toBe(redMessage); - expect(initializeMock).toHaveBeenCalledTimes(1); - expect(initializeMock).toHaveBeenCalledWith(firstLicenseCheckResults); - done(); - }); - - watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); -}); - -test(`calls initialize twice when the status changes to green twice`, (done) => { - const pluginId = 'foo-plugin'; - const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); - mockXpackMainPlugin.mock.setStatus('green'); - const licenseCheckResults = Symbol(); - mockFeature.mock.setLicenseCheckResults(licenseCheckResults); - const downstreamPlugin = createMockDownstreamPlugin(pluginId); - const initializeMock = jest.fn().mockImplementation(() => Promise.resolve()); - - let count = 0; - downstreamPlugin.status.green.mockImplementation((message) => { - expect(message).toBe('Ready'); - ++count; - if (count === 1) { - mockXpackMainPlugin.mock.setStatus('green'); - } - if (count === 2) { - expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults); - expect(initializeMock).toHaveBeenCalledTimes(2); - done(); - } - }); - - watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); -}); diff --git a/x-pack/legacy/server/lib/xpack_usage.js b/x-pack/legacy/server/lib/xpack_usage.js deleted file mode 100644 index 50b50ba18c37f..0000000000000 --- a/x-pack/legacy/server/lib/xpack_usage.js +++ /dev/null @@ -1,16 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -export function xpackUsage(client) { - /* - * Get an object over the Usage API that as available/enabled data and some - * select metadata for each of the X-Pack UI plugins - */ - return client.transport.request({ - method: 'GET', - path: '/_xpack/usage', - }); -} diff --git a/x-pack/plugins/actions/server/constants/plugin.ts b/x-pack/plugins/actions/server/constants/plugin.ts index 7d20eb6990247..b82464bd92a18 100644 --- a/x-pack/plugins/actions/server/constants/plugin.ts +++ b/x-pack/plugins/actions/server/constants/plugin.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LICENSE_TYPE_BASIC, LicenseType } from '../../../../legacy/common/constants'; +import { LicenseType } from '../../../licensing/server'; export const PLUGIN = { ID: 'actions', - MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType, // TODO: supposed to be changed up on requirements + MINIMUM_LICENSE_REQUIRED: 'basic' as LicenseType, // TODO: supposed to be changed up on requirements // eslint-disable-next-line @typescript-eslint/no-explicit-any getI18nName: (i18n: any): string => i18n.translate('xpack.actions.appName', { diff --git a/x-pack/plugins/alerts/server/constants/plugin.ts b/x-pack/plugins/alerts/server/constants/plugin.ts index c180b68680841..4e1e0c59e0b48 100644 --- a/x-pack/plugins/alerts/server/constants/plugin.ts +++ b/x-pack/plugins/alerts/server/constants/plugin.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LICENSE_TYPE_BASIC, LicenseType } from '../../../../legacy/common/constants'; +import { LicenseType } from '../../../licensing/server'; export const PLUGIN = { ID: 'alerts', - MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType, // TODO: supposed to be changed up on requirements + MINIMUM_LICENSE_REQUIRED: 'basic' as LicenseType, // TODO: supposed to be changed up on requirements // all plugins seem to use getI18nName with any // eslint-disable-next-line @typescript-eslint/no-explicit-any getI18nName: (i18n: any): string => diff --git a/x-pack/plugins/apm/common/service_health_status.ts b/x-pack/plugins/apm/common/service_health_status.ts index 468f06ab97af8..1d4bcfb3b0e07 100644 --- a/x-pack/plugins/apm/common/service_health_status.ts +++ b/x-pack/plugins/apm/common/service_health_status.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ANOMALY_SEVERITY } from '../../ml/common'; -import { EuiTheme } from '../../../legacy/common/eui_styled_components'; +import { EuiTheme } from '../../xpack_legacy/common'; export enum ServiceHealthStatus { healthy = 'healthy', diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx index a785cb31c3cf4..262d94d8f3674 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx @@ -27,10 +27,7 @@ import { SNAPSHOT_CUSTOM_AGGREGATIONS, SnapshotCustomAggregationRT, } from '../../../../../../../common/http_api/snapshot_api'; -import { - EuiTheme, - withTheme, -} from '../../../../../../../../../legacy/common/eui_styled_components'; +import { EuiTheme, withTheme } from '../../../../../../../../xpack_legacy/common'; interface SelectedOption { label: string; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx index e75885ccbc917..831a0cde49cfb 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx @@ -8,10 +8,7 @@ import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { getCustomMetricLabel } from '../../../../../../../common/formatters/get_custom_metric_label'; import { SnapshotCustomMetricInput } from '../../../../../../../common/http_api/snapshot_api'; -import { - EuiTheme, - withTheme, -} from '../../../../../../../../../legacy/common/eui_styled_components'; +import { EuiTheme, withTheme } from '../../../../../../../../xpack_legacy/common'; interface Props { theme: EuiTheme | undefined; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx index d1abcade5d660..956241545e8be 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx @@ -9,10 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { CustomMetricMode } from './types'; import { SnapshotCustomMetricInput } from '../../../../../../../common/http_api/snapshot_api'; -import { - EuiTheme, - withTheme, -} from '../../../../../../../../../legacy/common/eui_styled_components'; +import { EuiTheme, withTheme } from '../../../../../../../../xpack_legacy/common'; interface Props { theme: EuiTheme | undefined; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx index 45ac538d9e394..0bef3c20ddd1a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import styled from 'styled-components'; import { EuiErrorBoundary, EuiPanel, EuiEmptyPrompt, EuiCode } from '@elastic/eui'; import { CoreStart, AppMountParameters } from 'src/core/public'; -import { EuiThemeProvider } from '../../../../../legacy/common/eui_styled_components'; +import { EuiThemeProvider } from '../../../../xpack_legacy/common'; import { IngestManagerSetupDeps, IngestManagerConfigType, diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index fa691a7f41ddb..6cb7e8d9cf8fd 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -9,7 +9,7 @@ import ReactDOM from 'react-dom'; import { Route, Router, Switch } from 'react-router-dom'; import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public'; -import { EuiThemeProvider } from '../../../../legacy/common/eui_styled_components'; +import { EuiThemeProvider } from '../../../xpack_legacy/common'; import { PluginContext } from '../context/plugin_context'; import { usePluginContext } from '../hooks/use_plugin_context'; import { useRouteParams } from '../hooks/use_route_params'; diff --git a/x-pack/plugins/observability/public/hooks/use_theme.tsx b/x-pack/plugins/observability/public/hooks/use_theme.tsx index d0449a4432d93..51a1ad5029538 100644 --- a/x-pack/plugins/observability/public/hooks/use_theme.tsx +++ b/x-pack/plugins/observability/public/hooks/use_theme.tsx @@ -5,7 +5,7 @@ */ import { useContext } from 'react'; import { ThemeContext } from 'styled-components'; -import { EuiTheme } from '../../../../legacy/common/eui_styled_components'; +import { EuiTheme } from '../../../xpack_legacy/common'; export function useTheme() { const theme: EuiTheme = useContext(ThemeContext); diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx index 73c0f00573911..3b7262e8a8d7e 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_root_provider.tsx @@ -11,7 +11,7 @@ import { Router } from 'react-router-dom'; import { History } from 'history'; import { useObservable } from 'react-use'; import { Store } from 'redux'; -import { EuiThemeProvider } from '../../../../../../legacy/common/eui_styled_components'; +import { EuiThemeProvider } from '../../../../../xpack_legacy/common'; import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; import { RouteCapture } from '../../components/endpoint/route_capture'; import { StartPlugins } from '../../../types'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/vertical_divider.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/vertical_divider.ts index 6a3aecb4a6503..b6f5c9b7421b5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/vertical_divider.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/vertical_divider.ts @@ -5,7 +5,7 @@ */ import styled from 'styled-components'; -import { EuiTheme } from '../../../../../../../legacy/common/eui_styled_components'; +import { EuiTheme } from '../../../../../../xpack_legacy/common'; type SpacingOptions = keyof EuiTheme['eui']['spacerSizes']; diff --git a/x-pack/plugins/transform/common/constants.ts b/x-pack/plugins/transform/common/constants.ts index 5efb6f31c1e3f..f2dbc085ab46f 100644 --- a/x-pack/plugins/transform/common/constants.ts +++ b/x-pack/plugins/transform/common/constants.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; -import { LICENSE_TYPE_BASIC, LicenseType } from '../../../legacy/common/constants'; +import { LicenseType } from '../../licensing/common/types'; export const DEFAULT_REFRESH_INTERVAL_MS = 30000; export const MINIMUM_REFRESH_INTERVAL_MS = 1000; @@ -14,7 +14,7 @@ export const PROGRESS_REFRESH_INTERVAL_MS = 2000; export const PLUGIN = { ID: 'transform', - MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType, + MINIMUM_LICENSE_REQUIRED: 'basic' as LicenseType, getI18nName: (): string => { return i18n.translate('xpack.transform.appName', { defaultMessage: 'Transforms', diff --git a/x-pack/plugins/transform/server/routes/api/error_utils.ts b/x-pack/plugins/transform/server/routes/api/error_utils.ts index 269cd28c4bda6..ef5927651df88 100644 --- a/x-pack/plugins/transform/server/routes/api/error_utils.ts +++ b/x-pack/plugins/transform/server/routes/api/error_utils.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { boomify, isBoom } from 'boom'; +import Boom from 'boom'; import { i18n } from '@kbn/i18n'; @@ -76,10 +76,65 @@ export function fillResultsWithTimeouts({ results, id, items, action }: Params) } export function wrapError(error: any): CustomHttpResponseOptions { - const boom = isBoom(error) ? error : boomify(error, { statusCode: error.status }); + const boom = Boom.isBoom(error) ? error : Boom.boomify(error, { statusCode: error.status }); return { body: boom, headers: boom.output.headers, statusCode: boom.output.statusCode, }; } + +function extractCausedByChain( + causedBy: Record = {}, + accumulator: string[] = [] +): string[] { + const { reason, caused_by } = causedBy; // eslint-disable-line @typescript-eslint/naming-convention + + if (reason) { + accumulator.push(reason); + } + + if (caused_by) { + return extractCausedByChain(caused_by, accumulator); + } + + return accumulator; +} + +/** + * Wraps an error thrown by the ES JS client into a Boom error response and returns it + * + * @param err Object Error thrown by ES JS client + * @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages + * @return Object Boom error response + */ +export function wrapEsError(err: any, statusCodeToMessageMap: Record = {}) { + const { statusCode, response } = err; + + const { + error: { + root_cause = [], // eslint-disable-line @typescript-eslint/naming-convention + caused_by = {}, // eslint-disable-line @typescript-eslint/naming-convention + } = {}, + } = JSON.parse(response); + + // If no custom message if specified for the error's status code, just + // wrap the error as a Boom error response, include the additional information from ES, and return it + if (!statusCodeToMessageMap[statusCode]) { + const boomError = Boom.boomify(err, { statusCode }); + + // The caused_by chain has the most information so use that if it's available. If not then + // settle for the root_cause. + const causedByChain = extractCausedByChain(caused_by); + const defaultCause = root_cause.length ? extractCausedByChain(root_cause[0]) : undefined; + + // @ts-expect-error cause is not defined on payload type + boomError.output.payload.cause = causedByChain.length ? causedByChain : defaultCause; + return boomError; + } + + // Otherwise, use the custom message to create a Boom error response and + // return it + const message = statusCodeToMessageMap[statusCode]; + return new Boom(message, { statusCode }); +} diff --git a/x-pack/plugins/transform/server/routes/api/field_histograms.ts b/x-pack/plugins/transform/server/routes/api/field_histograms.ts index 88352ec4af129..636af095e0053 100644 --- a/x-pack/plugins/transform/server/routes/api/field_histograms.ts +++ b/x-pack/plugins/transform/server/routes/api/field_histograms.ts @@ -9,8 +9,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers'; - import { indexPatternTitleSchema, IndexPatternTitleSchema, @@ -24,7 +22,7 @@ import { RouteDependencies } from '../../types'; import { addBasePath } from '../index'; -import { wrapError } from './error_utils'; +import { wrapError, wrapEsError } from './error_utils'; export function registerFieldHistogramsRoutes({ router, license }: RouteDependencies) { router.post( diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index 3d2018eb5801f..31b2c2285a764 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -12,7 +12,6 @@ import { SavedObjectsClientContract, LegacyAPICaller, } from 'kibana/server'; -import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers'; import { TRANSFORM_STATE } from '../../../common/constants'; import { TransformId } from '../../../common/types/transform'; @@ -54,7 +53,7 @@ import { RouteDependencies } from '../../types'; import { addBasePath } from '../index'; -import { isRequestTimeout, fillResultsWithTimeouts, wrapError } from './error_utils'; +import { isRequestTimeout, fillResultsWithTimeouts, wrapError, wrapEsError } from './error_utils'; import { registerTransformsAuditMessagesRoutes } from './transforms_audit_messages'; import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; diff --git a/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts b/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts index f01b2bdb73fd5..20cb6ffb4978b 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts @@ -6,13 +6,12 @@ import { transformIdParamSchema, TransformIdParamSchema } from '../../../common/api_schemas/common'; import { AuditMessage, TransformMessage } from '../../../common/types/messages'; -import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers'; import { RouteDependencies } from '../../types'; import { addBasePath } from '../index'; -import { wrapError } from './error_utils'; +import { wrapError, wrapEsError } from './error_utils'; const ML_DF_NOTIFICATION_INDEX_PATTERN = '.transform-notifications-read'; const SIZE = 500; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 890ba48eccf1f..fe9eaf0615183 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -16673,9 +16673,6 @@ "xpack.securitySolution.zeek.sfDescription": "通常のSYN/FIN完了", "xpack.securitySolution.zeek.shDescription": "接続元がFINに続きSYNを送信しました。レスポンダーからSYN-ACKはありません", "xpack.securitySolution.zeek.shrDescription": "レスポンダーがFINに続きSYNを送信しました。接続元からSYN-ACKはありません", - "xpack.server.checkLicense.errorExpiredMessage": "{licenseType} ライセンスが期限切れのため {pluginName} を使用できません", - "xpack.server.checkLicense.errorUnavailableMessage": "現在ライセンス情報が利用できないため {pluginName} を使用できません。", - "xpack.server.checkLicense.errorUnsupportedMessage": "ご使用の {licenseType} ライセンスは {pluginName} をサポートしていません。ライセンスをアップグレードしてください。", "xpack.snapshotRestore.addPolicy.breadcrumbTitle": "ポリシーを追加", "xpack.snapshotRestore.addPolicy.loadingIndicesDescription": "利用可能なインデックスを読み込み中…", "xpack.snapshotRestore.addPolicy.LoadingIndicesErrorMessage": "利用可能なインデックスを読み込み中にエラーが発生", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 53e0f49d7877e..e1ae8c30960c2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -16683,9 +16683,6 @@ "xpack.securitySolution.zeek.sfDescription": "正常 SYN/FIN 完成", "xpack.securitySolution.zeek.shDescription": "发起方已发送 SYN,后跟 FIN,响应方未发送 SYN ACK", "xpack.securitySolution.zeek.shrDescription": "响应方已发送 SYN ACK,后跟 FIN,发起方未发送 SYN", - "xpack.server.checkLicense.errorExpiredMessage": "您不能使用 {pluginName},因为您的{licenseType}许可证已过期", - "xpack.server.checkLicense.errorUnavailableMessage": "您不能使用 {pluginName},因为许可证信息当前不可用。", - "xpack.server.checkLicense.errorUnsupportedMessage": "您的{licenseType}许可证不支持 {pluginName}。请升级您的许可证。", "xpack.snapshotRestore.addPolicy.breadcrumbTitle": "添加策略", "xpack.snapshotRestore.addPolicy.loadingIndicesDescription": "正在加载可用索引……", "xpack.snapshotRestore.addPolicy.LoadingIndicesErrorMessage": "加载可用索引时出错", diff --git a/x-pack/plugins/watcher/common/constants/plugin.ts b/x-pack/plugins/watcher/common/constants/plugin.ts index f89ef95e9261f..fa95b86c0673b 100644 --- a/x-pack/plugins/watcher/common/constants/plugin.ts +++ b/x-pack/plugins/watcher/common/constants/plugin.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LICENSE_TYPE_GOLD, LicenseType } from '../../../../legacy/common/constants'; +import { LicenseType } from '../../../licensing/common/types'; export const PLUGIN = { ID: 'watcher', - MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_GOLD as LicenseType, + MINIMUM_LICENSE_REQUIRED: 'gold' as LicenseType, getI18nName: (i18n: any): string => { return i18n.translate('xpack.watcher.appName', { defaultMessage: 'Watcher', diff --git a/x-pack/plugins/watcher/server/types.ts b/x-pack/plugins/watcher/server/types.ts index 167dcb3ab64c3..5ef3aef7de1c6 100644 --- a/x-pack/plugins/watcher/server/types.ts +++ b/x-pack/plugins/watcher/server/types.ts @@ -8,8 +8,6 @@ import { IRouter } from 'kibana/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; -import { XPackMainPlugin } from '../../../legacy/plugins/xpack_main/server/xpack_main'; - export interface Dependencies { licensing: LicensingPluginSetup; features: FeaturesPluginSetup; @@ -18,7 +16,6 @@ export interface Dependencies { export interface ServerShim { route: any; plugins: { - xpack_main: XPackMainPlugin; watcher: any; }; } diff --git a/x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx b/x-pack/plugins/xpack_legacy/common/eui_styled_components.tsx similarity index 100% rename from x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx rename to x-pack/plugins/xpack_legacy/common/eui_styled_components.tsx diff --git a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/index.js b/x-pack/plugins/xpack_legacy/common/index.ts similarity index 83% rename from x-pack/legacy/plugins/xpack_main/server/routes/api/v1/index.js rename to x-pack/plugins/xpack_legacy/common/index.ts index 80baf7bf1a64d..8c0dace27faf4 100644 --- a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/index.js +++ b/x-pack/plugins/xpack_legacy/common/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { xpackInfoRoute } from './xpack_info'; +export * from './eui_styled_components'; diff --git a/x-pack/tasks/build.ts b/x-pack/tasks/build.ts index 41bf9587cad1e..a3b08a16f4b08 100644 --- a/x-pack/tasks/build.ts +++ b/x-pack/tasks/build.ts @@ -61,9 +61,6 @@ async function copySourceAndBabelify() { 'plugins/**/*', 'plugins/reporting/.phantom/*', 'plugins/reporting/.chromium/*', - 'legacy/common/**/*', - 'legacy/plugins/**/*', - 'legacy/server/**/*', 'typings/**/*', ], { diff --git a/x-pack/tasks/helpers/flags.ts b/x-pack/tasks/helpers/flags.ts index 33ee126c2a2ee..8820a4d60aa40 100644 --- a/x-pack/tasks/helpers/flags.ts +++ b/x-pack/tasks/helpers/flags.ts @@ -4,14 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resolve } from 'path'; - import log from 'fancy-log'; import getopts from 'getopts'; -import { toArray } from 'rxjs/operators'; - -// @ts-ignore complicated module doesn't have types yet -import { findPluginSpecs } from '../../../src/legacy/plugin_discovery'; /* Usage: @@ -53,18 +47,3 @@ export const FLAGS = { .map((id) => id.trim()) : undefined, }; - -export async function getEnabledPlugins() { - if (FLAGS.plugins) { - return FLAGS.plugins; - } - - const { spec$ } = findPluginSpecs({ - plugins: { - paths: [resolve(__dirname, '..', '..')], - }, - }); - - const enabledPlugins: Array<{ getId: () => string }> = await spec$.pipe(toArray()).toPromise(); - return enabledPlugins.map((spec) => spec.getId()); -} diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 44c8449dc5dd0..7978a89231566 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -3,9 +3,6 @@ "include": [ "mocks.ts", "typings/**/*", - "legacy/common/**/*", - "legacy/server/**/*", - "legacy/plugins/**/*", "plugins/**/*", "test_utils/**/*", "tasks/**/*" @@ -21,7 +18,6 @@ "paths": { "kibana/public": ["src/core/public"], "kibana/server": ["src/core/server"], - "plugins/xpack_main/*": ["x-pack/legacy/plugins/xpack_main/public/*"], "test_utils/*": ["x-pack/test_utils/*"], "fixtures/*": ["src/fixtures/*"] }, diff --git a/x-pack/typings/hapi.d.ts b/x-pack/typings/hapi.d.ts index 253b639a52ff2..dd9e0239aeee7 100644 --- a/x-pack/typings/hapi.d.ts +++ b/x-pack/typings/hapi.d.ts @@ -6,7 +6,6 @@ import 'hapi'; -import { XPackMainPlugin } from '../legacy/plugins/xpack_main/server/xpack_main'; import { ActionsPlugin, ActionsClient } from '../plugins/actions/server'; import { AlertingPlugin, AlertsClient } from '../plugins/alerts/server'; import { TaskManager } from '../plugins/task_manager/server'; @@ -17,7 +16,6 @@ declare module 'hapi' { getAlertsClient?: () => AlertsClient; } interface PluginProperties { - xpack_main: XPackMainPlugin; actions?: ActionsPlugin; alerts?: AlertingPlugin; task_manager?: TaskManager; From 0f8043ca8dd2765d533b3fc7a0357a42f3710ff8 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Wed, 23 Sep 2020 10:23:55 +0200 Subject: [PATCH 34/92] [Lens] Combined histogram/range aggregation for numbers (#76121) Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com> Co-authored-by: Wylie Conlon Co-authored-by: Elastic Machine Co-authored-by: Marta Bondyra Co-authored-by: cchaos --- .../dimension_panel/bucket_nesting_editor.tsx | 8 + .../dimension_panel/dimension_editor.tsx | 1 - .../operations/definitions/date_histogram.tsx | 7 +- .../definitions/filters/filters.tsx | 1 + .../operations/definitions/index.ts | 4 + .../definitions/ranges/advanced_editor.scss | 6 + .../definitions/ranges/advanced_editor.tsx | 296 ++++++++++ .../definitions/ranges/constants.ts | 20 + .../operations/definitions/ranges/index.ts | 7 + .../definitions/ranges/range_editor.tsx | 175 ++++++ .../definitions/ranges/ranges.test.tsx | 555 ++++++++++++++++++ .../operations/definitions/ranges/ranges.tsx | 199 +++++++ .../definitions/shared_components/buckets.tsx | 3 + .../operations/definitions/terms.tsx | 6 +- .../operations/operations.test.ts | 30 +- .../xy_visualization/xy_config_panel.tsx | 5 +- .../public/xy_visualization/xy_expression.tsx | 72 ++- 17 files changed, 1368 insertions(+), 27 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.scss create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/advanced_editor.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/constants.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/index.ts create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx index 325f18ee9833a..3d692b1f7f5a8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx @@ -75,6 +75,10 @@ export function BucketNestingEditor({ defaultMessage: 'Top values for each {field}', values: { field: fieldName }, }), + range: i18n.translate('xpack.lens.indexPattern.groupingOverallRanges', { + defaultMessage: 'Top values for each {field}', + values: { field: fieldName }, + }), }; const bottomLevelCopy: Record = { @@ -90,6 +94,10 @@ export function BucketNestingEditor({ defaultMessage: 'Overall top {target}', values: { target: target.fieldName }, }), + range: i18n.translate('xpack.lens.indexPattern.groupingSecondRanges', { + defaultMessage: 'Overall top {target}', + values: { target: target.fieldName }, + }), }; return ( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 153757ac37da1..2f64a36e0462e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -332,7 +332,6 @@ export function DimensionEditor(props: DimensionEditorProps) { {!incompatibleSelectedOperationType && ParamEditor && ( <> - { if ( type === 'date' && @@ -180,7 +179,7 @@ export const dateHistogramOperation: OperationDefinition + <> {!intervalIsRestricted && ( )} - + ); }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx index cc1e23cb82a49..9985ad7229ecc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx @@ -226,6 +226,7 @@ export const FilterList = ({ removeTitle={i18n.translate('xpack.lens.indexPattern.filters.removeFilter', { defaultMessage: 'Remove a filter', })} + isNotRemovable={localFilters.length === 1} > + range.label || + formatter.convert({ + gte: isValidNumber(range.from) ? range.from : FROM_PLACEHOLDER, + lt: isValidNumber(range.to) ? range.to : TO_PLACEHOLDER, + }); + +export const RangePopover = ({ + range, + setRange, + Button, + isOpenByCreation, + setIsOpenByCreation, +}: { + range: LocalRangeType; + setRange: (newRange: LocalRangeType) => void; + Button: React.FunctionComponent<{ onClick: MouseEventHandler }>; + isOpenByCreation: boolean; + setIsOpenByCreation: (open: boolean) => void; + formatter: IFieldFormat; +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [tempRange, setTempRange] = useState(range); + + const saveRangeAndReset = (newRange: LocalRangeType, resetRange = false) => { + if (resetRange) { + // reset the temporary range for later use + setTempRange(range); + } + // send the range back to the main state + setRange(newRange); + }; + const { from, to } = tempRange; + + const lteAppendLabel = i18n.translate('xpack.lens.indexPattern.ranges.lessThanOrEqualAppend', { + defaultMessage: '\u2264', + }); + const lteTooltipContent = i18n.translate( + 'xpack.lens.indexPattern.ranges.lessThanOrEqualTooltip', + { + defaultMessage: 'Less than or equal to', + } + ); + const ltPrependLabel = i18n.translate('xpack.lens.indexPattern.ranges.lessThanPrepend', { + defaultMessage: '\u003c', + }); + const ltTooltipContent = i18n.translate('xpack.lens.indexPattern.ranges.lessThanTooltip', { + defaultMessage: 'Less than', + }); + + const onSubmit = () => { + setIsPopoverOpen(false); + setIsOpenByCreation(false); + saveRangeAndReset(tempRange, true); + }; + + return ( + { + setIsPopoverOpen((isOpen) => !isOpen); + setIsOpenByCreation(false); + }} + /> + } + data-test-subj="indexPattern-ranges-popover" + > + + + + { + const newRange = { + ...tempRange, + from: target.value !== '' ? Number(target.value) : -Infinity, + }; + setTempRange(newRange); + saveRangeAndReset(newRange); + }} + append={ + + {lteAppendLabel} + + } + fullWidth + compressed + placeholder={FROM_PLACEHOLDER} + isInvalid={!isValidRange(tempRange)} + /> + + + + + + { + const newRange = { + ...tempRange, + to: target.value !== '' ? Number(target.value) : -Infinity, + }; + setTempRange(newRange); + saveRangeAndReset(newRange); + }} + prepend={ + + {ltPrependLabel} + + } + fullWidth + compressed + placeholder={TO_PLACEHOLDER} + isInvalid={!isValidRange(tempRange)} + onKeyDown={({ key }: React.KeyboardEvent) => { + if (keys.ENTER === key && onSubmit) { + onSubmit(); + } + }} + /> + + + + + ); +}; + +export const AdvancedRangeEditor = ({ + ranges, + setRanges, + onToggleEditor, + formatter, +}: { + ranges: RangeTypeLens[]; + setRanges: (newRanges: RangeTypeLens[]) => void; + onToggleEditor: () => void; + formatter: IFieldFormat; +}) => { + // use a local state to store ids with range objects + const [localRanges, setLocalRanges] = useState(() => + ranges.map((range) => ({ ...range, id: generateId() })) + ); + // we need to force the open state of the popover from the outside in some scenarios + // so we need an extra state here + const [isOpenByCreation, setIsOpenByCreation] = useState(false); + + const lastIndex = localRanges.length - 1; + + // Update locally all the time, but bounce the parents prop function + // to aviod too many requests + useDebounce( + () => { + setRanges(localRanges.map(({ id, ...rest }) => ({ ...rest }))); + }, + TYPING_DEBOUNCE_TIME, + [localRanges] + ); + + const addNewRange = () => { + setLocalRanges([ + ...localRanges, + { + id: generateId(), + from: localRanges[localRanges.length - 1].to, + to: Infinity, + label: '', + }, + ]); + }; + + return ( + + + {' '} + {i18n.translate('xpack.lens.indexPattern.ranges.customIntervalsRemoval', { + defaultMessage: 'Remove custom intervals', + })} + + + } + > + <> + setIsOpenByCreation(false)} + droppableId="RANGES_DROPPABLE_AREA" + items={localRanges} + > + {localRanges.map((range: LocalRangeType, idx: number) => ( + { + const newRanges = localRanges.filter((_, i) => i !== idx); + setLocalRanges(newRanges); + }} + removeTitle={i18n.translate('xpack.lens.indexPattern.ranges.deleteRange', { + defaultMessage: 'Delete range', + })} + isNotRemovable={localRanges.length === 1} + > + { + const newRanges = [...localRanges]; + if (newRange.id === newRanges[idx].id) { + newRanges[idx] = newRange; + } else { + newRanges.push(newRange); + } + setLocalRanges(newRanges); + }} + formatter={formatter} + Button={({ onClick }: { onClick: MouseEventHandler }) => ( + + + {getBetterLabel(range, formatter)} + + + )} + /> + + ))} + + { + addNewRange(); + setIsOpenByCreation(true); + }} + label={i18n.translate('xpack.lens.indexPattern.ranges.addInterval', { + defaultMessage: 'Add interval', + })} + /> + + + ); +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/constants.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/constants.ts new file mode 100644 index 0000000000000..5c3c3c19a2b0f --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/constants.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const TYPING_DEBOUNCE_TIME = 256; +// Taken from the Visualize editor +export const FROM_PLACEHOLDER = '\u2212\u221E'; +export const TO_PLACEHOLDER = '+\u221E'; + +export const DEFAULT_INTERVAL = 1000; +export const AUTO_BARS = 'auto'; +export const MIN_HISTOGRAM_BARS = 1; +export const SLICES = 6; + +export const MODES = { + Range: 'range', + Histogram: 'histogram', +} as const; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/index.ts new file mode 100644 index 0000000000000..ccae0c949af0d --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './ranges'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx new file mode 100644 index 0000000000000..5d5acf7778973 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useDebounce } from 'react-use'; +import { + EuiButtonEmpty, + EuiFormRow, + EuiRange, + EuiFlexItem, + EuiFlexGroup, + EuiButtonIcon, + EuiToolTip, +} from '@elastic/eui'; +import { IFieldFormat } from 'src/plugins/data/public'; +import { RangeColumnParams, UpdateParamsFnType, MODES_TYPES } from './ranges'; +import { AdvancedRangeEditor } from './advanced_editor'; +import { TYPING_DEBOUNCE_TIME, MODES, MIN_HISTOGRAM_BARS } from './constants'; + +const BaseRangeEditor = ({ + maxBars, + step, + maxHistogramBars, + onToggleEditor, + onMaxBarsChange, +}: { + maxBars: number; + step: number; + maxHistogramBars: number; + onToggleEditor: () => void; + onMaxBarsChange: (newMaxBars: number) => void; +}) => { + const [maxBarsValue, setMaxBarsValue] = useState(String(maxBars)); + + useDebounce( + () => { + onMaxBarsChange(Number(maxBarsValue)); + }, + TYPING_DEBOUNCE_TIME, + [maxBarsValue] + ); + + const granularityLabel = i18n.translate('xpack.lens.indexPattern.ranges.granularity', { + defaultMessage: 'Granularity', + }); + const decreaseButtonLabel = i18n.translate('xpack.lens.indexPattern.ranges.decreaseButtonLabel', { + defaultMessage: 'Decrease granularity', + }); + const increaseButtonLabel = i18n.translate('xpack.lens.indexPattern.ranges.increaseButtonLabel', { + defaultMessage: 'Increase granularity', + }); + + return ( + <> + + + + + + setMaxBarsValue('' + Math.max(Number(maxBarsValue) - step, MIN_HISTOGRAM_BARS)) + } + aria-label={decreaseButtonLabel} + /> + + + + setMaxBarsValue(currentTarget.value)} + /> + + + + + setMaxBarsValue('' + Math.min(Number(maxBarsValue) + step, maxHistogramBars)) + } + aria-label={increaseButtonLabel} + /> + + + + + + onToggleEditor()}> + {i18n.translate('xpack.lens.indexPattern.ranges.customIntervalsToggle', { + defaultMessage: 'Create custom intervals', + })} + + + ); +}; + +export const RangeEditor = ({ + setParam, + params, + maxHistogramBars, + maxBars, + granularityStep, + onChangeMode, + rangeFormatter, +}: { + params: RangeColumnParams; + maxHistogramBars: number; + maxBars: number; + granularityStep: number; + setParam: UpdateParamsFnType; + onChangeMode: (mode: MODES_TYPES) => void; + rangeFormatter: IFieldFormat; +}) => { + const [isAdvancedEditor, toggleAdvancedEditor] = useState(params.type === MODES.Range); + + // if the maxBars in the params is set to auto refresh it with the default value + // only on bootstrap + useEffect(() => { + if (params.maxBars !== maxBars) { + setParam('maxBars', maxBars); + } + }, [maxBars, params.maxBars, setParam]); + + if (isAdvancedEditor) { + return ( + { + setParam('ranges', ranges); + }} + onToggleEditor={() => { + onChangeMode(MODES.Histogram); + toggleAdvancedEditor(false); + }} + formatter={rangeFormatter} + /> + ); + } + + return ( + { + setParam('maxBars', newMaxBars); + }} + onToggleEditor={() => { + onChangeMode(MODES.Range); + toggleAdvancedEditor(true); + }} + /> + ); +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx new file mode 100644 index 0000000000000..2409406afcdbc --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -0,0 +1,555 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { EuiFieldNumber, EuiRange, EuiButtonEmpty, EuiLink } from '@elastic/eui'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { IndexPatternPrivateState, IndexPattern } from '../../../types'; +import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; +import { rangeOperation } from '../index'; +import { RangeIndexPatternColumn } from './ranges'; +import { + MODES, + DEFAULT_INTERVAL, + TYPING_DEBOUNCE_TIME, + MIN_HISTOGRAM_BARS, + SLICES, +} from './constants'; +import { RangePopover } from './advanced_editor'; +import { DragDropBuckets } from '../shared_components'; + +const dataPluginMockValue = dataPluginMock.createStartContract(); +// need to overwrite the formatter field first +dataPluginMockValue.fieldFormats.deserialize = jest.fn().mockImplementation(() => { + return { convert: ({ gte, lt }: { gte: string; lt: string }) => `${gte} - ${lt}` }; +}); + +type ReactMouseEvent = React.MouseEvent & + React.MouseEvent; + +const defaultOptions = { + storage: {} as IStorageWrapper, + // need this for MAX_HISTOGRAM value + uiSettings: ({ + get: () => 100, + } as unknown) as IUiSettingsClient, + savedObjectsClient: {} as SavedObjectsClientContract, + dateRange: { + fromDate: 'now-1y', + toDate: 'now', + }, + data: dataPluginMockValue, + http: {} as HttpSetup, +}; + +describe('ranges', () => { + let state: IndexPatternPrivateState; + const InlineOptions = rangeOperation.paramEditor!; + const sourceField = 'MyField'; + const MAX_HISTOGRAM_VALUE = 100; + const GRANULARITY_DEFAULT_VALUE = (MAX_HISTOGRAM_VALUE - MIN_HISTOGRAM_BARS) / 2; + const GRANULARITY_STEP = (MAX_HISTOGRAM_VALUE - MIN_HISTOGRAM_BARS) / SLICES; + + function setToHistogramMode() { + const column = state.layers.first.columns.col1 as RangeIndexPatternColumn; + column.dataType = 'number'; + column.scale = 'interval'; + column.params.type = MODES.Histogram; + } + + function setToRangeMode() { + const column = state.layers.first.columns.col1 as RangeIndexPatternColumn; + column.dataType = 'string'; + column.scale = 'ordinal'; + column.params.type = MODES.Range; + } + + function getDefaultState(): IndexPatternPrivateState { + return { + indexPatternRefs: [], + indexPatterns: {}, + existingFields: {}, + currentIndexPatternId: '1', + isFirstExistenceFetch: false, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + // Start with the histogram type + col1: { + label: sourceField, + dataType: 'number', + operationType: 'range', + scale: 'interval', + isBucketed: true, + sourceField, + params: { + type: MODES.Histogram, + ranges: [{ from: 0, to: DEFAULT_INTERVAL, label: '' }], + maxBars: 'auto', + }, + }, + col2: { + label: 'Count', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }, + }, + }, + }; + } + + beforeAll(() => { + jest.useFakeTimers(); + }); + + beforeEach(() => { + state = getDefaultState(); + }); + + describe('toEsAggConfig', () => { + afterAll(() => setToHistogramMode()); + + it('should reflect params correctly', () => { + const esAggsConfig = rangeOperation.toEsAggsConfig( + state.layers.first.columns.col1 as RangeIndexPatternColumn, + 'col1', + {} as IndexPattern + ); + expect(esAggsConfig).toEqual( + expect.objectContaining({ + type: MODES.Histogram, + params: expect.objectContaining({ + field: sourceField, + maxBars: null, + }), + }) + ); + }); + + it('should reflect the type correctly', () => { + setToRangeMode(); + + const esAggsConfig = rangeOperation.toEsAggsConfig( + state.layers.first.columns.col1 as RangeIndexPatternColumn, + 'col1', + {} as IndexPattern + ); + + expect(esAggsConfig).toEqual( + expect.objectContaining({ + type: MODES.Range, + }) + ); + }); + }); + + describe('getPossibleOperationForField', () => { + it('should return operation with the right type for number', () => { + expect( + rangeOperation.getPossibleOperationForField({ + aggregatable: true, + searchable: true, + name: 'test', + displayName: 'test', + type: 'number', + }) + ).toEqual({ + dataType: 'number', + isBucketed: true, + scale: 'interval', + }); + }); + + it('should not return operation if field type is not number', () => { + expect( + rangeOperation.getPossibleOperationForField({ + aggregatable: false, + searchable: true, + name: 'test', + displayName: 'test', + type: 'string', + }) + ).toEqual(undefined); + }); + }); + + describe('paramEditor', () => { + describe('Modify intervals in basic mode', () => { + beforeEach(() => { + state = getDefaultState(); + }); + + it('should start update the state with the default maxBars value', () => { + const setStateSpy = jest.fn(); + mount( + + ); + + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...state.layers.first.columns.col1.params, + maxBars: GRANULARITY_DEFAULT_VALUE, + }, + }, + }, + }, + }, + }); + }); + + it('should update state when changing Max bars number', () => { + const setStateSpy = jest.fn(); + + const instance = mount( + + ); + + act(() => { + instance.find(EuiRange).prop('onChange')!( + { + currentTarget: { + value: '' + MAX_HISTOGRAM_VALUE, + }, + } as React.ChangeEvent, + true + ); + jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); + + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...state.layers.first.columns.col1.params, + maxBars: MAX_HISTOGRAM_VALUE, + }, + }, + }, + }, + }, + }); + }); + }); + + it('should update the state using the plus or minus buttons by the step amount', () => { + const setStateSpy = jest.fn(); + + const instance = mount( + + ); + + act(() => { + // minus button + instance + .find('[data-test-subj="lns-indexPattern-range-maxBars-minus"]') + .find('button') + .prop('onClick')!({} as ReactMouseEvent); + jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); + + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...state.layers.first.columns.col1.params, + maxBars: GRANULARITY_DEFAULT_VALUE - GRANULARITY_STEP, + }, + }, + }, + }, + }, + }); + + // plus button + instance + .find('[data-test-subj="lns-indexPattern-range-maxBars-plus"]') + .find('button') + .prop('onClick')!({} as ReactMouseEvent); + jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); + + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...state.layers.first.columns.col1.params, + maxBars: GRANULARITY_DEFAULT_VALUE, + }, + }, + }, + }, + }, + }); + }); + }); + }); + + describe('Specify range intervals manually', () => { + // @ts-expect-error + window['__react-beautiful-dnd-disable-dev-warnings'] = true; // issue with enzyme & react-beautiful-dnd throwing errors: https://github.com/atlassian/react-beautiful-dnd/issues/1593 + + beforeEach(() => setToRangeMode()); + + it('should show one range interval to start with', () => { + const setStateSpy = jest.fn(); + + const instance = mount( + + ); + + expect(instance.find(DragDropBuckets).children).toHaveLength(1); + }); + + it('should add a new range', () => { + const setStateSpy = jest.fn(); + + const instance = mount( + + ); + + // This series of act clojures are made to make it work properly the update flush + act(() => { + instance.find(EuiButtonEmpty).prop('onClick')!({} as ReactMouseEvent); + }); + + act(() => { + // need another wrapping for this in order to work + instance.update(); + + expect(instance.find(RangePopover)).toHaveLength(2); + + // edit the range and check + instance.find(RangePopover).find(EuiFieldNumber).first().prop('onChange')!({ + target: { + value: '50', + }, + } as React.ChangeEvent); + jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); + + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...state.layers.first.columns.col1.params, + ranges: [ + { from: 0, to: DEFAULT_INTERVAL, label: '' }, + { from: 50, to: Infinity, label: '' }, + ], + }, + }, + }, + }, + }, + }); + }); + }); + + it('should open a popover to edit an existing range', () => { + const setStateSpy = jest.fn(); + + const instance = mount( + + ); + + // This series of act clojures are made to make it work properly the update flush + act(() => { + instance.find(RangePopover).find(EuiLink).prop('onClick')!({} as ReactMouseEvent); + }); + + act(() => { + // need another wrapping for this in order to work + instance.update(); + + // edit the range "to" field + instance.find(RangePopover).find(EuiFieldNumber).last().prop('onChange')!({ + target: { + value: '50', + }, + } as React.ChangeEvent); + jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); + + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...state.layers.first.columns.col1.params, + ranges: [{ from: 0, to: 50, label: '' }], + }, + }, + }, + }, + }, + }); + }); + }); + + it('should not accept invalid ranges', () => { + const setStateSpy = jest.fn(); + + const instance = mount( + + ); + + // This series of act clojures are made to make it work properly the update flush + act(() => { + instance.find(RangePopover).find(EuiLink).prop('onClick')!({} as ReactMouseEvent); + }); + + act(() => { + // need another wrapping for this in order to work + instance.update(); + + // edit the range "to" field + instance.find(RangePopover).find(EuiFieldNumber).last().prop('onChange')!({ + target: { + value: '-1', + }, + } as React.ChangeEvent); + }); + + act(() => { + instance.update(); + + // and check + expect(instance.find(RangePopover).find(EuiFieldNumber).last().prop('isInvalid')).toBe( + true + ); + }); + }); + + it('should be possible to remove a range if multiple', () => { + const setStateSpy = jest.fn(); + + // Add an extra range + (state.layers.first.columns.col1 as RangeIndexPatternColumn).params.ranges.push({ + from: DEFAULT_INTERVAL, + to: 2 * DEFAULT_INTERVAL, + label: '', + }); + + const instance = mount( + + ); + + expect(instance.find(RangePopover)).toHaveLength(2); + + // This series of act closures are made to make it work properly the update flush + act(() => { + instance + .find('[data-test-subj="lns-customBucketContainer-remove"]') + .last() + .prop('onClick')!({} as ReactMouseEvent); + }); + + act(() => { + // need another wrapping for this in order to work + instance.update(); + + expect(instance.find(RangePopover)).toHaveLength(1); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx new file mode 100644 index 0000000000000..530c2e962759b --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -0,0 +1,199 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/common'; +import { Range } from '../../../../../../../../src/plugins/expressions/common/expression_types/index'; +import { RangeEditor } from './range_editor'; +import { OperationDefinition } from '../index'; +import { FieldBasedIndexPatternColumn } from '../column_types'; +import { updateColumnParam, changeColumn } from '../../../state_helpers'; +import { MODES, AUTO_BARS, DEFAULT_INTERVAL, MIN_HISTOGRAM_BARS, SLICES } from './constants'; + +type RangeType = Omit; +export type RangeTypeLens = RangeType & { label: string }; + +export type MODES_TYPES = typeof MODES[keyof typeof MODES]; + +export interface RangeIndexPatternColumn extends FieldBasedIndexPatternColumn { + operationType: 'range'; + params: { + type: MODES_TYPES; + maxBars: typeof AUTO_BARS | number; + ranges: RangeTypeLens[]; + }; +} + +export type RangeColumnParams = RangeIndexPatternColumn['params']; +export type UpdateParamsFnType = ( + paramName: K, + value: RangeColumnParams[K] +) => void; + +export const isValidNumber = (value: number | '') => + value !== '' && !isNaN(value) && isFinite(value); +export const isRangeWithin = (range: RangeTypeLens): boolean => range.from <= range.to; +const isFullRange = ({ from, to }: RangeType) => isValidNumber(from) && isValidNumber(to); +export const isValidRange = (range: RangeTypeLens): boolean => { + if (isFullRange(range)) { + return isRangeWithin(range); + } + return true; +}; + +function getEsAggsParams({ sourceField, params }: RangeIndexPatternColumn) { + if (params.type === MODES.Range) { + return { + field: sourceField, + ranges: params.ranges.filter(isValidRange).map>((range) => { + if (isFullRange(range)) { + return { from: range.from, to: range.to }; + } + const partialRange: Partial = {}; + // be careful with the fields to set on partial ranges + if (isValidNumber(range.from)) { + partialRange.from = range.from; + } + if (isValidNumber(range.to)) { + partialRange.to = range.to; + } + return partialRange; + }), + }; + } + return { + field: sourceField, + // fallback to 0 in case of empty string + maxBars: params.maxBars === AUTO_BARS ? null : params.maxBars, + has_extended_bounds: false, + min_doc_count: 0, + extended_bounds: { min: '', max: '' }, + }; +} + +export const rangeOperation: OperationDefinition = { + type: 'range', + displayName: i18n.translate('xpack.lens.indexPattern.ranges', { + defaultMessage: 'Ranges', + }), + priority: 4, // Higher than terms, so numbers get histogram + getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { + if ( + type === 'number' && + aggregatable && + (!aggregationRestrictions || aggregationRestrictions.range) + ) { + return { + dataType: 'number', + isBucketed: true, + scale: 'interval', + }; + } + }, + buildColumn({ suggestedPriority, field }) { + return { + label: field.name, + dataType: 'number', // string for Range + operationType: 'range', + suggestedPriority, + sourceField: field.name, + isBucketed: true, + scale: 'interval', // ordinal for Range + params: { + type: MODES.Histogram, + ranges: [{ from: 0, to: DEFAULT_INTERVAL, label: '' }], + maxBars: AUTO_BARS, + }, + }; + }, + isTransferable: (column, newIndexPattern) => { + const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField); + + return Boolean( + newField && + newField.type === 'number' && + newField.aggregatable && + (!newField.aggregationRestrictions || newField.aggregationRestrictions.range) + ); + }, + onFieldChange: (oldColumn, indexPattern, field) => { + return { + ...oldColumn, + label: field.name, + sourceField: field.name, + }; + }, + toEsAggsConfig: (column, columnId) => { + const params = getEsAggsParams(column); + return { + id: columnId, + enabled: true, + type: column.params.type, + schema: 'segment', + params, + }; + }, + paramEditor: ({ state, setState, currentColumn, layerId, columnId, uiSettings, data }) => { + const rangeFormatter = data.fieldFormats.deserialize({ id: 'range' }); + const MAX_HISTOGRAM_BARS = uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS); + const granularityStep = (MAX_HISTOGRAM_BARS - MIN_HISTOGRAM_BARS) / SLICES; + const maxBarsDefaultValue = (MAX_HISTOGRAM_BARS - MIN_HISTOGRAM_BARS) / 2; + + // Used to change one param at the time + const setParam: UpdateParamsFnType = (paramName, value) => { + setState( + updateColumnParam({ + state, + layerId, + currentColumn, + paramName, + value, + }) + ); + }; + + // Useful to change more params at once + const onChangeMode = (newMode: MODES_TYPES) => { + const scale = newMode === MODES.Range ? 'ordinal' : 'interval'; + const dataType = newMode === MODES.Range ? 'string' : 'number'; + setState( + changeColumn({ + state, + layerId, + columnId, + newColumn: { + ...currentColumn, + scale, + dataType, + params: { + type: newMode, + ranges: [{ from: 0, to: DEFAULT_INTERVAL, label: '' }], + maxBars: maxBarsDefaultValue, + }, + }, + keepParams: false, + }) + ); + }; + return ( + + ); + }, +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.tsx index 73378cea919a6..47380f7865578 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.tsx @@ -35,6 +35,7 @@ interface BucketContainerProps { invalidMessage: string; onRemoveClick: () => void; removeTitle: string; + isNotRemovable?: boolean; children: React.ReactNode; dataTestSubj?: string; } @@ -46,6 +47,7 @@ const BucketContainer = ({ removeTitle, children, dataTestSubj, + isNotRemovable, }: BucketContainerProps) => { return ( @@ -75,6 +77,7 @@ const BucketContainer = ({ onClick={onRemoveClick} aria-label={removeTitle} title={removeTitle} + disabled={isNotRemovable} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx index 20c421008a746..c1a87a2013747 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiForm, EuiFormRow, EuiRange, EuiSelect } from '@elastic/eui'; +import { EuiFormRow, EuiRange, EuiSelect } from '@elastic/eui'; import { IndexPatternColumn } from '../../indexpattern'; import { updateColumnParam } from '../../state_helpers'; import { DataType } from '../../../types'; @@ -171,7 +171,7 @@ export const termsOperation: OperationDefinition = { }), }); return ( - + <> = { })} /> - + ); }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 4ac3fc89500f9..703431f724c5d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -225,29 +225,43 @@ describe('getOperationTypesForField', () => { it('should list out all field-operation tuples for different operation meta data', () => { expect(getAvailableOperationsByMetadata(expectedIndexPatterns[1])).toMatchInlineSnapshot(` Array [ + Object { + "operationMetaData": Object { + "dataType": "date", + "isBucketed": true, + "scale": "interval", + }, + "operations": Array [ + Object { + "field": "timestamp", + "operationType": "date_histogram", + "type": "field", + }, + ], + }, Object { "operationMetaData": Object { "dataType": "number", "isBucketed": true, - "scale": "ordinal", + "scale": "interval", }, "operations": Array [ Object { "field": "bytes", - "operationType": "terms", + "operationType": "range", "type": "field", }, ], }, Object { "operationMetaData": Object { - "dataType": "string", + "dataType": "number", "isBucketed": true, "scale": "ordinal", }, "operations": Array [ Object { - "field": "source", + "field": "bytes", "operationType": "terms", "type": "field", }, @@ -255,14 +269,14 @@ describe('getOperationTypesForField', () => { }, Object { "operationMetaData": Object { - "dataType": "date", + "dataType": "string", "isBucketed": true, - "scale": "interval", + "scale": "ordinal", }, "operations": Array [ Object { - "field": "timestamp", - "operationType": "date_histogram", + "field": "source", + "operationType": "terms", "type": "field", }, ], diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index c7781c2e1d50c..ee22ee51301df 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -17,7 +17,6 @@ import { EuiFormRow, EuiText, htmlIdGenerator, - EuiForm, EuiColorPicker, EuiColorPickerProps, EuiToolTip, @@ -366,7 +365,7 @@ export function DimensionEditor(props: VisualizationDimensionEditorProps) 'auto'; return ( - + <> ) }} /> - + ); } diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index 9379c8a612eb2..24bf78dba2121 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -24,6 +24,7 @@ import { ExpressionFunctionDefinition, ExpressionRenderDefinition, ExpressionValueSearchContext, + KibanaDatatable, } from 'src/plugins/expressions/public'; import { IconType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -251,6 +252,12 @@ export function XYChart({ ({ id }) => id === filteredLayers[0].xAccessor ); const xAxisFormatter = formatFactory(xAxisColumn && xAxisColumn.formatHint); + const layersAlreadyFormatted: Record = {}; + // This is a safe formatter for the xAccessor that abstracts the knowledge of already formatted layers + const safeXAccessorLabelRenderer = (value: unknown): string => + xAxisColumn && layersAlreadyFormatted[xAxisColumn.id] + ? (value as string) + : xAxisFormatter.convert(value); const chartHasMoreThanOneSeries = filteredLayers.length > 1 || @@ -364,7 +371,7 @@ export function XYChart({ theme={chartTheme} baseTheme={chartBaseTheme} tooltip={{ - headerFormatter: (d) => xAxisFormatter.convert(d.value), + headerFormatter: (d) => safeXAccessorLabelRenderer(d.value), }} rotation={shouldRotate ? 90 : 0} xDomain={xDomain} @@ -409,9 +416,15 @@ export function XYChart({ const points = [ { - row: table.rows.findIndex( - (row) => layer.xAccessor && row[layer.xAccessor] === xyGeometry.x - ), + row: table.rows.findIndex((row) => { + if (layer.xAccessor) { + if (layersAlreadyFormatted[layer.xAccessor]) { + // stringify the value to compare with the chart value + return xAxisFormatter.convert(row[layer.xAccessor]) === xyGeometry.x; + } + return row[layer.xAccessor] === xyGeometry.x; + } + }), column: table.columns.findIndex((col) => col.id === layer.xAccessor), value: xyGeometry.x, }, @@ -455,7 +468,7 @@ export function XYChart({ strokeWidth: 2, }} hide={filteredLayers[0].hide || !filteredLayers[0].xAccessor} - tickFormat={(d) => xAxisFormatter.convert(d)} + tickFormat={(d) => safeXAccessorLabelRenderer(d)} style={{ tickLabel: { visible: tickLabelsVisibilitySettings?.x, @@ -504,9 +517,43 @@ export function XYChart({ const table = data.tables[layerId]; + const isPrimitive = (value: unknown): boolean => + value != null && typeof value !== 'object'; + + // what if row values are not primitive? That is the case of, for instance, Ranges + // remaps them to their serialized version with the formatHint metadata + // In order to do it we need to make a copy of the table as the raw one is required for more features (filters, etc...) later on + const tableConverted: KibanaDatatable = { + ...table, + rows: table.rows.map((row) => { + const newRow = { ...row }; + for (const column of table.columns) { + const record = newRow[column.id]; + if (record && !isPrimitive(record)) { + newRow[column.id] = formatFactory(column.formatHint).convert(record); + } + } + return newRow; + }), + }; + + // save the id of the layer with the custom table + table.columns.reduce>( + (alreadyFormatted: Record, { id }) => { + if (alreadyFormatted[id]) { + return alreadyFormatted; + } + alreadyFormatted[id] = table.rows.some( + (row, i) => row[id] !== tableConverted.rows[i][id] + ); + return alreadyFormatted; + }, + layersAlreadyFormatted + ); + // For date histogram chart type, we're getting the rows that represent intervals without data. // To not display them in the legend, they need to be filtered out. - const rows = table.rows.filter( + const rows = tableConverted.rows.filter( (row) => !(xAccessor && typeof row[xAccessor] === 'undefined') && !( @@ -559,19 +606,28 @@ export function XYChart({ // * Key - Y name // * Formatted value - Y name if (accessors.length > 1) { - return d.seriesKeys + const result = d.seriesKeys .map((key: string | number, i) => { - if (i === 0 && splitHint) { + if ( + i === 0 && + splitHint && + splitAccessor && + !layersAlreadyFormatted[splitAccessor] + ) { return formatFactory(splitHint).convert(key); } return splitAccessor && i === 0 ? key : columnToLabelMap[key] ?? ''; }) .join(' - '); + return result; } // For formatted split series, format the key // This handles splitting by dates, for example if (splitHint) { + if (splitAccessor && layersAlreadyFormatted[splitAccessor]) { + return d.seriesKeys[0]; + } return formatFactory(splitHint).convert(d.seriesKeys[0]); } // This handles both split and single-y cases: From 4b6d77fa5d5e3a830ccaabad863252044f7c1523 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 23 Sep 2020 11:12:12 +0200 Subject: [PATCH 35/92] [Drilldowns] Config to disable URL Drilldown (#77887) This pr makes sure there is way to disable URL drilldown feature. I decided to extract Url drilldown definition into a separate plugin to benefit from regular disabling a plugin feature. Having it as a separate plugin also makes sense because we will start adding registries specific to URL drilldown implementation Co-authored-by: Elastic Machine --- docs/developer/plugin-list.asciidoc | 4 ++ docs/user/dashboard/url-drilldown.asciidoc | 11 ++++ src/plugins/ui_actions/public/mocks.ts | 1 + .../public/service/ui_actions_service.ts | 4 ++ x-pack/.i18nrc.json | 1 + .../drilldowns/url_drilldown/README.md | 16 ++--- .../drilldowns/url_drilldown/kibana.json | 8 +++ .../url_drilldown/public}/index.ts | 7 ++- .../url_drilldown/public/lib}/i18n.ts | 9 +-- .../url_drilldown/public/lib}/index.ts | 0 .../public/lib}/url_drilldown.test.ts | 0 .../public/lib}/url_drilldown.tsx | 0 .../public/lib}/url_drilldown_scope.test.ts | 0 .../public/lib}/url_drilldown_scope.ts | 0 .../drilldowns/url_drilldown/public/plugin.ts | 59 +++++++++++++++++++ .../plugins/embeddable_enhanced/kibana.json | 3 +- .../embeddable_enhanced/public/plugin.ts | 16 ----- .../dynamic_action_manager.test.ts | 17 +++++- .../dynamic_actions/dynamic_action_manager.ts | 19 +++++- .../ui_actions_enhanced/public/mocks.ts | 1 + .../ui_actions_enhanced/public/plugin.ts | 7 ++- .../ui_actions_service_enhancements.ts | 4 ++ 22 files changed, 151 insertions(+), 36 deletions(-) rename x-pack/plugins/{embeddable_enhanced/public => }/drilldowns/url_drilldown/README.md (65%) create mode 100644 x-pack/plugins/drilldowns/url_drilldown/kibana.json rename x-pack/plugins/{embeddable_enhanced/public/drilldowns => drilldowns/url_drilldown/public}/index.ts (53%) rename x-pack/plugins/{embeddable_enhanced/public/drilldowns/url_drilldown => drilldowns/url_drilldown/public/lib}/i18n.ts (62%) rename x-pack/plugins/{embeddable_enhanced/public/drilldowns/url_drilldown => drilldowns/url_drilldown/public/lib}/index.ts (100%) rename x-pack/plugins/{embeddable_enhanced/public/drilldowns/url_drilldown => drilldowns/url_drilldown/public/lib}/url_drilldown.test.ts (100%) rename x-pack/plugins/{embeddable_enhanced/public/drilldowns/url_drilldown => drilldowns/url_drilldown/public/lib}/url_drilldown.tsx (100%) rename x-pack/plugins/{embeddable_enhanced/public/drilldowns/url_drilldown => drilldowns/url_drilldown/public/lib}/url_drilldown_scope.test.ts (100%) rename x-pack/plugins/{embeddable_enhanced/public/drilldowns/url_drilldown => drilldowns/url_drilldown/public/lib}/url_drilldown_scope.ts (100%) create mode 100644 x-pack/plugins/drilldowns/url_drilldown/public/plugin.ts diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index b426621fed296..5a4a60c2e628e 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -504,6 +504,10 @@ in their infrastructure. |Contains HTTP endpoints and UiSettings that are slated for removal. +|{kib-repo}blob/{branch}/x-pack/plugins/drilldowns/url_drilldown/README.md[urlDrilldown] +|NOTE: This plugin contains implementation of URL drilldown. For drilldowns infrastructure code refer to ui_actions_enhanced plugin. + + |=== include::{kibana-root}/src/plugins/dashboard/README.asciidoc[leveloffset=+1] diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc index e6daf89d72718..ee879256a1fae 100644 --- a/docs/user/dashboard/url-drilldown.asciidoc +++ b/docs/user/dashboard/url-drilldown.asciidoc @@ -238,3 +238,14 @@ Tip: Consider using <> helper for date formatting. | Aggregation field behind the selected range, if available. |=== + +[float] +[[disable]] +==== Disable URL drilldown + +You can disable URL drilldown feature on your {kib} instance by disabling the plugin: + +["source","yml"] +----------- +url_drilldown.enabled: false +----------- diff --git a/src/plugins/ui_actions/public/mocks.ts b/src/plugins/ui_actions/public/mocks.ts index 3522ac4941ba0..759430169b613 100644 --- a/src/plugins/ui_actions/public/mocks.ts +++ b/src/plugins/ui_actions/public/mocks.ts @@ -48,6 +48,7 @@ const createStartContract = (): Start => { executeTriggerActions: jest.fn(), fork: jest.fn(), getAction: jest.fn(), + hasAction: jest.fn(), getTrigger: jest.fn(), getTriggerActions: jest.fn((id: TriggerId) => []), getTriggerCompatibleActions: jest.fn(), diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.ts b/src/plugins/ui_actions/public/service/ui_actions_service.ts index 6028177964fb7..ec5f3afa19c94 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts @@ -99,6 +99,10 @@ export class UiActionsService { this.actions.delete(actionId); }; + public readonly hasAction = (actionId: string): boolean => { + return this.actions.has(actionId); + }; + public readonly attachAction = (triggerId: T, actionId: string): void => { const trigger = this.triggers.get(triggerId); diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index b0124546944ae..66ae478b86828 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -55,6 +55,7 @@ "xpack.triggersActionsUI": "plugins/triggers_actions_ui", "xpack.upgradeAssistant": "plugins/upgrade_assistant", "xpack.uptime": ["plugins/uptime"], + "xpack.urlDrilldown": "plugins/drilldowns/url_drilldown", "xpack.watcher": "plugins/watcher", "xpack.observability": "plugins/observability" }, diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/README.md b/x-pack/plugins/drilldowns/url_drilldown/README.md similarity index 65% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/README.md rename to x-pack/plugins/drilldowns/url_drilldown/README.md index 996723ccb914d..8eedc44ca35ae 100644 --- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/README.md +++ b/x-pack/plugins/drilldowns/url_drilldown/README.md @@ -1,24 +1,26 @@ -# Basic url drilldown implementation +## URL drilldown + +> NOTE: This plugin contains implementation of URL drilldown. For drilldowns infrastructure code refer to `ui_actions_enhanced` plugin. Url drilldown allows navigating to external URL or to internal kibana URL. By using variables in url template result url can be dynamic and depend on user's interaction. URL drilldown has 3 sources for variables: -- Global static variables like, for example, `kibanaUrl`. Such variables won’t change depending on a place where url drilldown is used. -- Context variables are dynamic and different depending on where drilldown is created and used. -- Event variables depend on a trigger context. These variables are dynamically extracted from the action context when drilldown is executed. +1. Global static variables like, for example, `kibanaUrl`. Such variables won’t change depending on a place where url drilldown is used. +2. Context variables are dynamic and different depending on where drilldown is created and used. +3. Event variables depend on a trigger context. These variables are dynamically extracted from the action context when drilldown is executed. Difference between `event` and `context` variables, is that real `context` variables are available during drilldown creation (e.g. embeddable panel), but `event` variables mapped from trigger context. Since there is no trigger context during drilldown creation, we have to provide some _mock_ variables for validating and previewing the URL. In current implementation url drilldown has to be used inside the embeddable and with `ValueClickTrigger` or `RangeSelectTrigger`. -- `context` variables extracted from `embeddable` -- `event` variables extracted from `trigger` context +* `context` variables extracted from `embeddable` +* `event` variables extracted from `trigger` context In future this basic url drilldown implementation would allow injecting more variables into `context` (e.g. `dashboard` app specific variables) and would allow providing support for new trigger types from outside. This extensibility improvements are tracked here: https://github.com/elastic/kibana/issues/55324 In case a solution app has a use case for url drilldown that has to be different from current basic implementation and -just extending variables list is not enough, then recommendation is to create own custom url drilldown and reuse building blocks from `ui_actions_enhanced`. +just extending variables list is not enough, then recommendation is to create own custom url drilldown and reuse building blocks from `ui_actions_enhanced`. \ No newline at end of file diff --git a/x-pack/plugins/drilldowns/url_drilldown/kibana.json b/x-pack/plugins/drilldowns/url_drilldown/kibana.json new file mode 100644 index 0000000000000..9bdd13fbfea26 --- /dev/null +++ b/x-pack/plugins/drilldowns/url_drilldown/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "urlDrilldown", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["embeddable", "uiActions", "uiActionsEnhanced"], + "requiredBundles": ["kibanaUtils", "kibanaReact"] +} diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts b/x-pack/plugins/drilldowns/url_drilldown/public/index.ts similarity index 53% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts rename to x-pack/plugins/drilldowns/url_drilldown/public/index.ts index a8d5a179dbac1..b040ef625bc1f 100644 --- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/index.ts @@ -4,4 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './url_drilldown'; +import { PluginInitializerContext } from 'src/core/public'; +import { UrlDrilldownPlugin } from './plugin'; + +export function plugin(context: PluginInitializerContext) { + return new UrlDrilldownPlugin(context); +} diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/i18n.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/i18n.ts similarity index 62% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/i18n.ts rename to x-pack/plugins/drilldowns/url_drilldown/public/lib/i18n.ts index 748f6f4cecedd..7e91c6b849035 100644 --- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/i18n.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/i18n.ts @@ -6,9 +6,6 @@ import { i18n } from '@kbn/i18n'; -export const txtUrlDrilldownDisplayName = i18n.translate( - 'xpack.embeddableEnhanced.drilldowns.urlDrilldownDisplayName', - { - defaultMessage: 'Go to URL', - } -); +export const txtUrlDrilldownDisplayName = i18n.translate('xpack.urlDrilldown.DisplayName', { + defaultMessage: 'Go to URL', +}); diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/index.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/index.ts similarity index 100% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/index.ts rename to x-pack/plugins/drilldowns/url_drilldown/public/lib/index.ts diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts similarity index 100% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts rename to x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx similarity index 100% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx rename to x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts similarity index 100% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.ts rename to x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts similarity index 100% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts rename to x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/plugin.ts b/x-pack/plugins/drilldowns/url_drilldown/public/plugin.ts new file mode 100644 index 0000000000000..82ce7a129f497 --- /dev/null +++ b/x-pack/plugins/drilldowns/url_drilldown/public/plugin.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { + AdvancedUiActionsSetup, + AdvancedUiActionsStart, + urlDrilldownGlobalScopeProvider, +} from '../../../ui_actions_enhanced/public'; +import { UrlDrilldown } from './lib'; +import { createStartServicesGetter } from '../../../../../src/plugins/kibana_utils/public'; + +export interface SetupDependencies { + embeddable: EmbeddableSetup; + uiActionsEnhanced: AdvancedUiActionsSetup; +} + +export interface StartDependencies { + embeddable: EmbeddableStart; + uiActionsEnhanced: AdvancedUiActionsStart; +} + +// eslint-disable-next-line +export interface SetupContract {} + +// eslint-disable-next-line +export interface StartContract {} + +export class UrlDrilldownPlugin + implements Plugin { + constructor(protected readonly context: PluginInitializerContext) {} + + public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { + const startServices = createStartServicesGetter(core.getStartServices); + plugins.uiActionsEnhanced.registerDrilldown( + new UrlDrilldown({ + getGlobalScope: urlDrilldownGlobalScopeProvider({ core }), + navigateToUrl: (url: string) => + core.getStartServices().then(([{ application }]) => application.navigateToUrl(url)), + getSyntaxHelpDocsLink: () => + startServices().core.docLinks.links.dashboard.urlDrilldownTemplateSyntax, + getVariablesHelpDocsLink: () => + startServices().core.docLinks.links.dashboard.urlDrilldownVariables, + }) + ); + + return {}; + } + + public start(core: CoreStart, plugins: StartDependencies): StartContract { + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/embeddable_enhanced/kibana.json b/x-pack/plugins/embeddable_enhanced/kibana.json index acada946fe0d1..8d49e3e26eb7b 100644 --- a/x-pack/plugins/embeddable_enhanced/kibana.json +++ b/x-pack/plugins/embeddable_enhanced/kibana.json @@ -3,6 +3,5 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["embeddable", "kibanaReact", "uiActions", "uiActionsEnhanced"], - "requiredBundles": ["kibanaUtils"] + "requiredPlugins": ["embeddable", "kibanaReact", "uiActions", "uiActionsEnhanced"] } diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts index 2138a372523b7..5d5ad852839d4 100644 --- a/x-pack/plugins/embeddable_enhanced/public/plugin.ts +++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts @@ -28,11 +28,8 @@ import { UiActionsEnhancedDynamicActionManager as DynamicActionManager, AdvancedUiActionsSetup, AdvancedUiActionsStart, - urlDrilldownGlobalScopeProvider, } from '../../ui_actions_enhanced/public'; import { PanelNotificationsAction, ACTION_PANEL_NOTIFICATIONS } from './actions'; -import { UrlDrilldown } from './drilldowns'; -import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; declare module '../../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { @@ -64,23 +61,10 @@ export class EmbeddableEnhancedPlugin public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { this.setCustomEmbeddableFactoryProvider(plugins); - const startServices = createStartServicesGetter(core.getStartServices); const panelNotificationAction = new PanelNotificationsAction(); plugins.uiActionsEnhanced.registerAction(panelNotificationAction); plugins.uiActionsEnhanced.attachAction(PANEL_NOTIFICATION_TRIGGER, panelNotificationAction.id); - plugins.uiActionsEnhanced.registerDrilldown( - new UrlDrilldown({ - getGlobalScope: urlDrilldownGlobalScopeProvider({ core }), - navigateToUrl: (url: string) => - core.getStartServices().then(([{ application }]) => application.navigateToUrl(url)), - getSyntaxHelpDocsLink: () => - startServices().core.docLinks.links.dashboard.urlDrilldownTemplateSyntax, - getVariablesHelpDocsLink: () => - startServices().core.docLinks.links.dashboard.urlDrilldownVariables, - }) - ); - return {}; } diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts index 83232bbce1ba7..cdd357f3560b8 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts @@ -437,8 +437,7 @@ describe('DynamicActionManager', () => { name: 'foo', config: {}, }; - - await expect(manager.createEvent(action, ['SELECT_RANGE_TRIGGER'])).rejects; + await expect(manager.createEvent(action, ['SELECT_RANGE_TRIGGER'])).rejects.toThrow(); }); }); }); @@ -704,4 +703,18 @@ describe('DynamicActionManager', () => { expect(basicAndGoldActions).toHaveLength(2); }); + + test("failing to revive/kill an action doesn't fail action manager", async () => { + const { manager, uiActions, storage } = setup([event1, event3, event2]); + + uiActions.registerActionFactory(actionFactoryDefinition1); + + await manager.start(); + + expect(uiActions.getTriggerActions('VALUE_CLICK_TRIGGER')).toHaveLength(2); + expect(await storage.list()).toEqual([event1, event3, event2]); + + await manager.stop(); + expect(uiActions.getTriggerActions('VALUE_CLICK_TRIGGER')).toHaveLength(0); + }); }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts index 471b929fdbc06..b414296690c9e 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts @@ -34,7 +34,13 @@ export interface DynamicActionManagerParams { storage: ActionStorage; uiActions: Pick< StartContract, - 'registerAction' | 'attachAction' | 'unregisterAction' | 'detachAction' | 'getActionFactory' + | 'registerAction' + | 'attachAction' + | 'unregisterAction' + | 'detachAction' + | 'hasAction' + | 'getActionFactory' + | 'hasActionFactory' >; isCompatible: (context: C) => Promise; } @@ -73,8 +79,17 @@ export class DynamicActionManager { const actionId = this.generateActionId(eventId); + if (!uiActions.hasActionFactory(action.factoryId)) { + // eslint-disable-next-line no-console + console.warn( + `Action factory for action [action.factoryId = ${action.factoryId}] doesn't exist. Skipping action [action.name = ${action.name}] revive.` + ); + return; + } + const factory = uiActions.getActionFactory(event.action.factoryId); const actionDefinition: ActionDefinition = factory.create(action as SerializedAction); + uiActions.registerAction({ ...actionDefinition, id: actionId, @@ -100,6 +115,7 @@ export class DynamicActionManager { protected killAction({ eventId, triggers }: SerializedEvent) { const { uiActions } = this.params; const actionId = this.generateActionId(eventId); + if (!uiActions.hasAction(actionId)) return; for (const trigger of triggers) uiActions.detachAction(trigger as any, actionId); uiActions.unregisterAction(actionId); @@ -157,6 +173,7 @@ export class DynamicActionManager { try { const events = await this.params.storage.list(); for (const event of events) this.reviveAction(event); + this.ui.transitions.finishFetching(events); } catch (error) { this.ui.transitions.failFetching(error instanceof Error ? error : { message: String(error) }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts index 9eb0a06b6dbaf..1900f04b0c7d8 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts @@ -29,6 +29,7 @@ const createStartContract = (): Start => { ...uiActionsPluginMock.createStartContract(), getActionFactories: jest.fn(), getActionFactory: jest.fn(), + hasActionFactory: jest.fn(), FlyoutManageDrilldowns: jest.fn(), telemetry: jest.fn(), extract: jest.fn(), diff --git a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts index b05c08c4c77d0..31236d2ea9779 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts @@ -61,7 +61,12 @@ export interface StartContract extends UiActionsStart, Pick< UiActionsServiceEnhancements, - 'getActionFactory' | 'getActionFactories' | 'telemetry' | 'extract' | 'inject' + | 'getActionFactory' + | 'hasActionFactory' + | 'getActionFactories' + | 'telemetry' + | 'extract' + | 'inject' > { FlyoutManageDrilldowns: ReturnType; } diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts index 5e40d803962de..cbbd88e65e841 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts @@ -79,6 +79,10 @@ export class UiActionsServiceEnhancements return actionFactory; }; + public readonly hasActionFactory = (actionFactoryId: string): boolean => { + return this.actionFactories.has(actionFactoryId); + }; + /** * Returns an array of all action factories. */ From 9276a16db74eb17dfdc0980d02f895840e1b7397 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 23 Sep 2020 12:51:39 +0200 Subject: [PATCH 36/92] [CSM] Url search (#77516) Co-authored-by: Justin Kambic Co-authored-by: Elastic Machine --- .github/CODEOWNERS | 3 +- .../cypress/integration/csm_dashboard.feature | 8 + .../step_definitions/csm/url_search_filter.ts | 65 +++++++ .../app/RumDashboard/ClientMetrics/index.tsx | 11 +- .../PageLoadDistribution/index.tsx | 12 +- .../PageLoadDistribution/use_breakdowns.ts | 5 +- .../app/RumDashboard/PageViewsTrend/index.tsx | 5 +- .../URLFilter}/ServiceNameFilter/index.tsx | 4 +- .../URLFilter/URLSearch/RenderOption.tsx | 68 ++++++++ .../URLFilter/URLSearch/SelectableUrlList.tsx | 164 ++++++++++++++++++ .../URLFilter/URLSearch/index.tsx | 132 ++++++++++++++ .../app/RumDashboard/URLFilter/UrlList.tsx | 74 ++++++++ .../app/RumDashboard/URLFilter/index.tsx | 102 +++++++++++ .../RumDashboard/UXMetrics/KeyUXMetrics.tsx | 11 +- .../app/RumDashboard/UXMetrics/index.tsx | 11 +- .../RumDashboard/VisitorBreakdown/index.tsx | 5 +- .../components/app/RumDashboard/index.tsx | 9 +- .../app/RumDashboard/translations.ts | 26 +++ .../lib/rum_client/get_client_metrics.ts | 3 + .../lib/rum_client/get_long_task_metrics.ts | 37 ++-- .../rum_client/get_page_load_distribution.ts | 3 + .../lib/rum_client/get_page_view_trends.ts | 1 + .../lib/rum_client/get_pl_dist_breakdown.ts | 3 + .../server/lib/rum_client/get_url_search.ts | 67 +++++++ .../lib/rum_client/get_visitor_breakdown.ts | 3 + .../lib/rum_client/get_web_core_vitals.ts | 2 + .../projections/rum_page_load_transactions.ts | 13 ++ .../apm/server/routes/create_apm_api.ts | 2 + .../plugins/apm/server/routes/rum_client.ts | 76 ++++++-- .../trial/tests/csm/url_search.ts | 90 ++++++++++ .../apm_api_integration/trial/tests/index.ts | 1 + 31 files changed, 966 insertions(+), 50 deletions(-) create mode 100644 x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts rename x-pack/plugins/apm/public/components/{shared/LocalUIFilters => app/RumDashboard/URLFilter}/ServiceNameFilter/index.tsx (94%) create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/RenderOption.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/UrlList.tsx create mode 100644 x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx create mode 100644 x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts create mode 100644 x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0898cfc97f916..f4e620dea95a9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -65,11 +65,12 @@ # Client Side Monitoring (lives in APM directories but owned by Uptime) /x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm @elastic/uptime +/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature @elastic/uptime /x-pack/plugins/apm/public/application/csmApp.tsx @elastic/uptime /x-pack/plugins/apm/public/components/app/RumDashboard @elastic/uptime /x-pack/plugins/apm/server/lib/rum_client @elastic/uptime /x-pack/plugins/apm/server/routes/rum_client.ts @elastic/uptime -/x-pack/plugins/apm/server/projections/rum_overview.ts @elastic/uptime +/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @elastic/uptime # Beats /x-pack/plugins/beats_management/ @elastic/beats diff --git a/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature b/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature index ac4188a598458..7b894b6ca7aac 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature +++ b/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature @@ -27,3 +27,11 @@ Feature: CSM Dashboard Given a user clicks the page load breakdown filter When the user selected the breakdown Then breakdown series should appear in chart + + Scenario: Search by url filter focus + When a user clicks inside url search field + Then it displays top pages in the suggestion popover + + Scenario: Search by url filter + When a user enters a query in url search field + Then it should filter results based on query diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts new file mode 100644 index 0000000000000..3b5dd70065055 --- /dev/null +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { When, Then } from 'cypress-cucumber-preprocessor/steps'; +import { DEFAULT_TIMEOUT } from './csm_dashboard'; + +When(`a user clicks inside url search field`, () => { + // wait for all loading to finish + cy.get('kbnLoadingIndicator').should('not.be.visible'); + cy.get('.euiStat__title-isLoading').should('not.be.visible'); + cy.get('span[data-cy=csmUrlFilter]', DEFAULT_TIMEOUT).within(() => { + cy.get('input.euiFieldSearch').click(); + }); +}); + +Then(`it displays top pages in the suggestion popover`, () => { + cy.get('kbnLoadingIndicator').should('not.be.visible'); + + cy.get('div.euiPopover__panel-isOpen', DEFAULT_TIMEOUT).within(() => { + const listOfUrls = cy.get('li.euiSelectableListItem'); + listOfUrls.should('have.length', 5); + + const actualUrlsText = [ + 'http://opbeans-node:3000/dashboardPage views: 17Page load duration: 109 ms ', + 'http://opbeans-node:3000/ordersPage views: 14Page load duration: 72 ms', + ]; + + cy.get('li.euiSelectableListItem') + .eq(0) + .should('have.text', actualUrlsText[0]); + cy.get('li.euiSelectableListItem') + .eq(1) + .should('have.text', actualUrlsText[1]); + }); +}); + +When(`a user enters a query in url search field`, () => { + cy.get('kbnLoadingIndicator').should('not.be.visible'); + + cy.get('[data-cy=csmUrlFilter]').within(() => { + cy.get('input.euiSelectableSearch').type('cus'); + }); + + cy.get('kbnLoadingIndicator').should('not.be.visible'); +}); + +Then(`it should filter results based on query`, () => { + cy.get('kbnLoadingIndicator').should('not.be.visible'); + + cy.get('div.euiPopover__panel-isOpen', DEFAULT_TIMEOUT).within(() => { + const listOfUrls = cy.get('li.euiSelectableListItem'); + listOfUrls.should('have.length', 1); + + const actualUrlsText = [ + 'http://opbeans-node:3000/customersPage views: 10Page load duration: 76 ms ', + ]; + + cy.get('li.euiSelectableListItem') + .eq(0) + .should('have.text', actualUrlsText[0]); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx index 1edfd724dadd7..a77d27c4bc883 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx @@ -22,7 +22,7 @@ const ClFlexGroup = styled(EuiFlexGroup)` export function ClientMetrics() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, searchTerm } = urlParams; const { data, status } = useFetcher( (callApmApi) => { @@ -31,13 +31,18 @@ export function ClientMetrics() { return callApmApi({ pathname: '/api/apm/rum/client-metrics', params: { - query: { start, end, uiFilters: JSON.stringify(uiFilters) }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, + }, }, }); } return Promise.resolve(null); }, - [start, end, uiFilters] + [start, end, uiFilters, searchTerm] ); const STAT_STYLE = { width: '240px' }; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index f97db3b42eecb..c8e45d2b2f672 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -22,7 +22,7 @@ export interface PercentileRange { export function PageLoadDistribution() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, searchTerm } = urlParams; const [percentileRange, setPercentileRange] = useState({ min: null, @@ -41,6 +41,7 @@ export function PageLoadDistribution() { start, end, uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, ...(percentileRange.min && percentileRange.max ? { minPercentile: String(percentileRange.min), @@ -53,7 +54,14 @@ export function PageLoadDistribution() { } return Promise.resolve(null); }, - [end, start, uiFilters, percentileRange.min, percentileRange.max] + [ + end, + start, + uiFilters, + percentileRange.min, + percentileRange.max, + searchTerm, + ] ); const onPercentileChange = (min: number, max: number) => { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts index 814cf977c9569..d6a544333531f 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/use_breakdowns.ts @@ -17,7 +17,7 @@ interface Props { export const useBreakdowns = ({ percentileRange, field, value }: Props) => { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, searchTerm } = urlParams; const { min: minP, max: maxP } = percentileRange ?? {}; @@ -32,6 +32,7 @@ export const useBreakdowns = ({ percentileRange, field, value }: Props) => { end, breakdown: value, uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, ...(minP && maxP ? { minPercentile: String(minP), @@ -43,6 +44,6 @@ export const useBreakdowns = ({ percentileRange, field, value }: Props) => { }); } }, - [end, start, uiFilters, field, value, minP, maxP] + [end, start, uiFilters, field, value, minP, maxP, searchTerm] ); }; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx index 2991f9a15f085..f2da0955412e7 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -16,7 +16,7 @@ import { BreakdownItem } from '../../../../../typings/ui_filters'; export function PageViewsTrend() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, searchTerm } = urlParams; const [breakdown, setBreakdown] = useState(null); @@ -30,6 +30,7 @@ export function PageViewsTrend() { start, end, uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, ...(breakdown ? { breakdowns: JSON.stringify(breakdown), @@ -41,7 +42,7 @@ export function PageViewsTrend() { } return Promise.resolve(undefined); }, - [end, start, uiFilters, breakdown] + [end, start, uiFilters, breakdown, searchTerm] ); return ( diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx rename to x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx index cbf9ba009dce2..f10c9e888a193 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx @@ -13,8 +13,8 @@ import { import { i18n } from '@kbn/i18n'; import React, { useEffect, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { fromQuery, toQuery } from '../../Links/url_helpers'; +import { useUrlParams } from '../../../../../hooks/useUrlParams'; +import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers'; interface Props { serviceNames: string[]; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/RenderOption.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/RenderOption.tsx new file mode 100644 index 0000000000000..1a6f4e25fc315 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/RenderOption.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ReactNode } from 'react'; +import classNames from 'classnames'; +import { EuiHighlight, EuiSelectableOption } from '@elastic/eui'; +import styled from 'styled-components'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +const StyledSpan = styled.span` + color: ${euiLightVars.euiColorSecondaryText}; + font-weight: 500; + :not(:last-of-type)::after { + content: '•'; + margin: 0 4px; + } +`; + +const StyledListSpan = styled.span` + display: block; + margin-top: 4px; + font-size: 12px; +`; +export type UrlOption = { + meta?: string[]; +} & EuiSelectableOption; + +export const formatOptions = (options: EuiSelectableOption[]) => { + return options.map((item: EuiSelectableOption) => ({ + title: item.label, + ...item, + className: classNames( + 'euiSelectableTemplateSitewide__listItem', + item.className + ), + })); +}; + +export function selectableRenderOptions( + option: UrlOption, + searchValue: string +) { + return ( + <> + + {option.label} + + {renderOptionMeta(option.meta)} + + ); +} + +function renderOptionMeta(meta?: string[]): ReactNode { + if (!meta || meta.length < 1) return; + return ( + + {meta.map((item: string) => ( + {item} + ))} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx new file mode 100644 index 0000000000000..298ec15b8480b --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FormEvent, useRef, useState } from 'react'; +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPopover, + EuiPopoverTitle, + EuiSelectable, + EuiSelectableMessage, +} from '@elastic/eui'; +import { + formatOptions, + selectableRenderOptions, + UrlOption, +} from './RenderOption'; +import { I18LABELS } from '../../translations'; + +interface Props { + data: { + items: UrlOption[]; + total?: number; + }; + loading: boolean; + onInputChange: (e: FormEvent) => void; + onTermChange: () => void; + onChange: (updatedOptions: UrlOption[]) => void; + searchValue: string; + onClose: () => void; +} + +export function SelectableUrlList({ + data, + loading, + onInputChange, + onTermChange, + onChange, + searchValue, + onClose, +}: Props) { + const [popoverIsOpen, setPopoverIsOpen] = useState(false); + const [popoverRef, setPopoverRef] = useState(null); + const [searchRef, setSearchRef] = useState(null); + + const titleRef = useRef(null); + + const searchOnFocus = (e: React.FocusEvent) => { + setPopoverIsOpen(true); + }; + + const onSearchInput = (e: React.FormEvent) => { + onInputChange(e); + setPopoverIsOpen(true); + }; + + const searchOnBlur = (e: React.FocusEvent) => { + if ( + !popoverRef?.contains(e.relatedTarget as HTMLElement) && + !popoverRef?.contains(titleRef.current as HTMLDivElement) + ) { + setPopoverIsOpen(false); + } + }; + + const formattedOptions = formatOptions(data.items ?? []); + + const closePopover = () => { + setPopoverIsOpen(false); + onClose(); + if (searchRef) { + searchRef.blur(); + } + }; + + const loadingMessage = ( + + +
+

{I18LABELS.loadingResults}

+
+ ); + + const emptyMessage = ( + +

{I18LABELS.noResults}

+
+ ); + + const titleText = searchValue + ? I18LABELS.getSearchResultsLabel(data?.total ?? 0) + : I18LABELS.topPages; + + function PopOverTitle() { + return ( + + + + {loading ? : titleText} + + + { + onTermChange(); + setPopoverIsOpen(false); + }} + > + {I18LABELS.matchThisQuery} + + + + + ); + } + + return ( + + {(list, search) => ( + +
+ + {list} +
+
+ )} +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx new file mode 100644 index 0000000000000..b88cf29740dcd --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiTitle } from '@elastic/eui'; +import useDebounce from 'react-use/lib/useDebounce'; +import React, { useEffect, useState, FormEvent, useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; +import { useUrlParams } from '../../../../../hooks/useUrlParams'; +import { useFetcher } from '../../../../../hooks/useFetcher'; +import { I18LABELS } from '../../translations'; +import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers'; +import { formatToSec } from '../../UXMetrics/KeyUXMetrics'; +import { SelectableUrlList } from './SelectableUrlList'; +import { UrlOption } from './RenderOption'; + +interface Props { + onChange: (value: string[]) => void; +} + +export function URLSearch({ onChange: onFilterChange }: Props) { + const history = useHistory(); + + const { urlParams, uiFilters } = useUrlParams(); + + const { start, end, serviceName } = urlParams; + const [searchValue, setSearchValue] = useState(''); + + const [debouncedValue, setDebouncedValue] = useState(''); + + useDebounce( + () => { + setSearchValue(debouncedValue); + }, + 250, + [debouncedValue] + ); + + const updateSearchTerm = useCallback( + (searchTermN: string) => { + const newLocation = { + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + searchTerm: searchTermN, + }), + }; + history.push(newLocation); + }, + [history] + ); + + const [checkedUrls, setCheckedUrls] = useState([]); + + const { data, status } = useFetcher( + (callApmApi) => { + if (start && end && serviceName) { + const { transactionUrl, ...restFilters } = uiFilters; + + return callApmApi({ + pathname: '/api/apm/rum-client/url-search', + params: { + query: { + start, + end, + uiFilters: JSON.stringify(restFilters), + urlQuery: searchValue, + }, + }, + }); + } + return Promise.resolve(null); + }, + [start, end, serviceName, uiFilters, searchValue] + ); + + useEffect(() => { + setCheckedUrls(uiFilters.transactionUrl || []); + }, [uiFilters]); + + const onChange = (updatedOptions: UrlOption[]) => { + const clickedItems = updatedOptions.filter( + (option) => option.checked === 'on' + ); + + setCheckedUrls(clickedItems.map((item) => item.url)); + }; + + const items: UrlOption[] = (data?.items ?? []).map((item) => ({ + label: item.url, + key: item.url, + meta: [ + I18LABELS.pageViews + ': ' + item.count, + I18LABELS.pageLoadDuration + ': ' + formatToSec(item.pld), + ], + url: item.url, + checked: checkedUrls?.includes(item.url) ? 'on' : undefined, + })); + + const onInputChange = (e: FormEvent) => { + setDebouncedValue(e.currentTarget.value); + }; + + const isLoading = status !== 'success'; + + const onTermChange = () => { + updateSearchTerm(searchValue); + }; + + const onClose = () => { + onFilterChange(checkedUrls); + }; + + return ( + <> + +

{I18LABELS.url}

+
+ + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/UrlList.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/UrlList.tsx new file mode 100644 index 0000000000000..437c005db37b0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/UrlList.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexGrid, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { px, truncate, unit } from '../../../../style/variables'; + +const BadgeText = styled.div` + display: inline-block; + ${truncate(px(unit * 12))}; + vertical-align: middle; +`; + +interface Props { + value: string[]; + onRemove: (val: string) => void; +} + +const formatUrlValue = (val: string) => { + const maxUrlToDisplay = 30; + const urlLength = val.length; + if (urlLength < maxUrlToDisplay) { + return val; + } + const urlObj = new URL(val); + if (urlObj.pathname === '/') { + return val; + } + const domainVal = urlObj.hostname; + const extraLength = urlLength - maxUrlToDisplay; + const extraDomain = domainVal.substring(0, extraLength); + + if (urlObj.pathname.length + 7 > maxUrlToDisplay) { + return val.replace(domainVal, '..'); + } + + return val.replace(extraDomain, '..'); +}; + +const removeFilterLabel = i18n.translate( + 'xpack.apm.uifilter.badge.removeFilter', + { defaultMessage: 'Remove filter' } +); + +export function UrlList({ onRemove, value }: Props) { + return ( + + {value.map((val) => ( + + { + onRemove(val); + }} + onClickAriaLabel={removeFilterLabel} + iconOnClick={() => { + onRemove(val); + }} + iconOnClickAriaLabel={removeFilterLabel} + iconType="cross" + iconSide="right" + > + {formatUrlValue(val)} + + + ))} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx new file mode 100644 index 0000000000000..9d3c8d012871f --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React, { useCallback, useMemo } from 'react'; +import { EuiSpacer, EuiBadge } from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; +import { Projection } from '../../../../../common/projections'; +import { useLocalUIFilters } from '../../../../hooks/useLocalUIFilters'; +import { URLSearch } from './URLSearch'; +import { LocalUIFilters } from '../../../shared/LocalUIFilters'; +import { UrlList } from './UrlList'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; + +const removeSearchTermLabel = i18n.translate( + 'xpack.apm.uiFilter.url.removeSearchTerm', + { defaultMessage: 'Clear url query' } +); + +export function URLFilter() { + const history = useHistory(); + + const { + urlParams: { searchTerm }, + } = useUrlParams(); + + const localUIFiltersConfig = useMemo(() => { + const config: React.ComponentProps = { + filterNames: ['transactionUrl'], + projection: Projection.rumOverview, + }; + + return config; + }, []); + + const { filters, setFilterValue } = useLocalUIFilters({ + ...localUIFiltersConfig, + }); + + const updateSearchTerm = useCallback( + (searchTermN?: string) => { + const newLocation = { + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + searchTerm: searchTermN, + }), + }; + history.push(newLocation); + }, + [history] + ); + + const { name, value: filterValue } = filters[0]; + + return ( + + + { + setFilterValue('transactionUrl', value); + }} + /> + + {searchTerm && ( + <> + { + updateSearchTerm(); + }} + onClickAriaLabel={removeSearchTermLabel} + iconOnClick={() => { + updateSearchTerm(); + }} + iconOnClickAriaLabel={removeSearchTermLabel} + iconType="cross" + iconSide="right" + > + *{searchTerm}* + + + + )} + {filterValue.length > 0 && ( + { + setFilterValue( + name, + filterValue.filter((v) => val !== v) + ); + }} + value={filterValue} + /> + )} + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx index 5c9a636adec8f..1d8360872afba 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx @@ -38,7 +38,7 @@ interface Props { export function KeyUXMetrics({ data, loading }: Props) { const { urlParams, uiFilters } = useUrlParams(); - const { start, end, serviceName } = urlParams; + const { start, end, serviceName, searchTerm } = urlParams; const { data: longTaskData, status } = useFetcher( (callApmApi) => { @@ -46,13 +46,18 @@ export function KeyUXMetrics({ data, loading }: Props) { return callApmApi({ pathname: '/api/apm/rum-client/long-task-metrics', params: { - query: { start, end, uiFilters: JSON.stringify(uiFilters) }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, + }, }, }); } return Promise.resolve(null); }, - [start, end, serviceName, uiFilters] + [start, end, serviceName, uiFilters, searchTerm] ); // Note: FCP value is in ms unit diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx index 94c3acfaa9727..3c7b4e39401de 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx @@ -33,7 +33,7 @@ export interface UXMetrics { export function UXMetrics() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, searchTerm } = urlParams; const { data, status } = useFetcher( (callApmApi) => { @@ -42,13 +42,18 @@ export function UXMetrics() { return callApmApi({ pathname: '/api/apm/rum-client/web-core-vitals', params: { - query: { start, end, uiFilters: JSON.stringify(uiFilters) }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, + }, }, }); } return Promise.resolve(null); }, - [start, end, uiFilters] + [start, end, uiFilters, searchTerm] ); return ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx index 245f58370d3d7..2db6ef8fa6c06 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx @@ -14,7 +14,7 @@ import { useUrlParams } from '../../../../hooks/useUrlParams'; export function VisitorBreakdown() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, searchTerm } = urlParams; const { data, status } = useFetcher( (callApmApi) => { @@ -26,13 +26,14 @@ export function VisitorBreakdown() { start, end, uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, }, }, }); } return Promise.resolve(null); }, - [end, start, uiFilters] + [end, start, uiFilters, searchTerm] ); return ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index fa0551252b6a1..588831d55771d 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -12,14 +12,15 @@ import { EuiSpacer, } from '@elastic/eui'; import { useTrackPageview } from '../../../../../observability/public'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { Projection } from '../../../../common/projections'; import { RumDashboard } from './RumDashboard'; -import { ServiceNameFilter } from '../../shared/LocalUIFilters/ServiceNameFilter'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useFetcher } from '../../../hooks/useFetcher'; import { RUM_AGENTS } from '../../../../common/agent_name'; import { EnvironmentFilter } from '../../shared/EnvironmentFilter'; +import { URLFilter } from './URLFilter'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { ServiceNameFilter } from './URLFilter/ServiceNameFilter'; export function RumOverview() { useTrackPageview({ app: 'apm', path: 'rum_overview' }); @@ -27,7 +28,7 @@ export function RumOverview() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { - filterNames: ['transactionUrl', 'location', 'device', 'os', 'browser'], + filterNames: ['location', 'device', 'os', 'browser'], projection: Projection.rumOverview, }; @@ -63,6 +64,7 @@ export function RumOverview() { + <> + {' '} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts index 1fafb7d1ed4d0..714788ef468c6 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts @@ -79,6 +79,32 @@ export const I18LABELS = { defaultMessage: 'Page load duration by region', } ), + searchByUrl: i18n.translate('xpack.apm.rum.filters.searchByUrl', { + defaultMessage: 'Search by url', + }), + getSearchResultsLabel: (total: number) => + i18n.translate('xpack.apm.rum.filters.searchResults', { + defaultMessage: '{total} Search results', + values: { total }, + }), + topPages: i18n.translate('xpack.apm.rum.filters.topPages', { + defaultMessage: 'Top pages', + }), + select: i18n.translate('xpack.apm.rum.filters.select', { + defaultMessage: 'Select', + }), + url: i18n.translate('xpack.apm.rum.filters.url', { + defaultMessage: 'Url', + }), + matchThisQuery: i18n.translate('xpack.apm.rum.filters.url.matchThisQuery', { + defaultMessage: 'Match this query', + }), + loadingResults: i18n.translate('xpack.apm.rum.filters.url.loadingResults', { + defaultMessage: 'Loading results', + }), + noResults: i18n.translate('xpack.apm.rum.filters.url.noResults', { + defaultMessage: 'No results available', + }), }; export const VisitorBreakdownLabel = i18n.translate( diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts index b3f9646f64029..cf4a5538a208d 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts @@ -19,11 +19,14 @@ import { export async function getClientMetrics({ setup, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; }) { const projection = getRumPageLoadTransactionsProjection({ setup, + urlQuery, }); const params = mergeProjection(projection, { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts index 1faee52034580..812cf9865bda8 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts @@ -14,12 +14,17 @@ import { SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; -import { SPAN_DURATION } from '../../../common/elasticsearch_fieldnames'; +import { + SPAN_DURATION, + TRANSACTION_ID, +} from '../../../common/elasticsearch_fieldnames'; export async function getLongTaskMetrics({ setup, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; }) { const projection = getRumLongTasksProjection({ setup, @@ -28,9 +33,6 @@ export async function getLongTaskMetrics({ const params = mergeProjection(projection, { body: { size: 0, - query: { - bool: projection.body.query.bool, - }, aggs: { transIds: { terms: { @@ -59,10 +61,13 @@ export async function getLongTaskMetrics({ const response = await apmEventClient.search(params); const { transIds } = response.aggregations ?? {}; - const validTransactions: string[] = await filterPageLoadTransactions( + const validTransactions: string[] = await filterPageLoadTransactions({ setup, - (transIds?.buckets ?? []).map((bucket) => bucket.key as string) - ); + urlQuery, + transactionIds: (transIds?.buckets ?? []).map( + (bucket) => bucket.key as string + ), + }); let noOfLongTasks = 0; let sumOfLongTasks = 0; let longestLongTask = 0; @@ -83,12 +88,18 @@ export async function getLongTaskMetrics({ }; } -async function filterPageLoadTransactions( - setup: Setup & SetupTimeRange & SetupUIFilters, - transactionIds: string[] -) { +async function filterPageLoadTransactions({ + setup, + urlQuery, + transactionIds, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; + transactionIds: string[]; +}) { const projection = getRumPageLoadTransactionsProjection({ setup, + urlQuery, }); const params = mergeProjection(projection, { @@ -99,14 +110,14 @@ async function filterPageLoadTransactions( must: [ { terms: { - 'transaction.id': transactionIds, + [TRANSACTION_ID]: transactionIds, }, }, ], filter: [...projection.body.query.bool.filter], }, }, - _source: ['transaction.id'], + _source: [TRANSACTION_ID], }, }); diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts index 3d8ab7a72654d..25de9f06fefc4 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts @@ -40,13 +40,16 @@ export async function getPageLoadDistribution({ setup, minPercentile, maxPercentile, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; minPercentile?: string; maxPercentile?: string; + urlQuery?: string; }) { const projection = getRumPageLoadTransactionsProjection({ setup, + urlQuery, }); const params = mergeProjection(projection, { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts index 543aa911b0b1f..ef4f8b16e0e7b 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -18,6 +18,7 @@ export async function getPageViewTrends({ }: { setup: Setup & SetupTimeRange & SetupUIFilters; breakdowns?: string; + urlQuery?: string; }) { const projection = getRumPageLoadTransactionsProjection({ setup, diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts index 1945140e35777..d59817cc682a9 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts @@ -44,11 +44,13 @@ export const getPageLoadDistBreakdown = async ({ minDuration, maxDuration, breakdown, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; minDuration: number; maxDuration: number; breakdown: string; + urlQuery?: string; }) => { // convert secs to micros const stepValues = getPLDChartSteps({ @@ -58,6 +60,7 @@ export const getPageLoadDistBreakdown = async ({ const projection = getRumPageLoadTransactionsProjection({ setup, + urlQuery, }); const params = mergeProjection(projection, { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts b/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts new file mode 100644 index 0000000000000..a7117f275c17b --- /dev/null +++ b/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mergeProjection } from '../../projections/util/merge_projection'; +import { + Setup, + SetupTimeRange, + SetupUIFilters, +} from '../helpers/setup_request'; +import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; + +export async function getUrlSearch({ + setup, + urlQuery, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; +}) { + const projection = getRumPageLoadTransactionsProjection({ + setup, + urlQuery, + }); + + const params = mergeProjection(projection, { + body: { + size: 0, + aggs: { + totalUrls: { + cardinality: { + field: 'url.full', + }, + }, + urls: { + terms: { + field: 'url.full', + size: 10, + }, + aggs: { + medianPLD: { + percentiles: { + field: 'transaction.duration.us', + percents: [50], + }, + }, + }, + }, + }, + }, + }); + + const { apmEventClient } = setup; + + const response = await apmEventClient.search(params); + const { urls, totalUrls } = response.aggregations ?? {}; + + return { + total: totalUrls?.value || 0, + items: (urls?.buckets ?? []).map((bucket) => ({ + url: bucket.key as string, + count: bucket.doc_count, + pld: bucket.medianPLD.values['50.0'] ?? 0, + })), + }; +} diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts index 3493307929f42..1b4388afd7c5d 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts @@ -19,11 +19,14 @@ import { export async function getVisitorBreakdown({ setup, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; }) { const projection = getRumPageLoadTransactionsProjection({ setup, + urlQuery, }); const params = mergeProjection(projection, { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts index 2ff0173b9ac12..fa34c2e25fecd 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts @@ -22,8 +22,10 @@ import { export async function getWebCoreVitals({ setup, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; }) { const projection = getRumPageLoadTransactionsProjection({ setup, diff --git a/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts index 27cd9b53f8349..3c3eaaca7efdb 100644 --- a/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts +++ b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @@ -19,8 +19,10 @@ import { TRANSACTION_PAGE_LOAD } from '../../common/transaction_types'; export function getRumPageLoadTransactionsProjection({ setup, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; }) { const { start, end, uiFiltersES } = setup; @@ -35,6 +37,17 @@ export function getRumPageLoadTransactionsProjection({ field: 'transaction.marks.navigationTiming.fetchStart', }, }, + ...(urlQuery + ? [ + { + wildcard: { + 'url.full': { + value: `*${urlQuery}*`, + }, + }, + }, + ] + : []), ...uiFiltersES, ], }; diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index 7d9a9ccc167e0..f975ab177f147 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -77,6 +77,7 @@ import { rumServicesRoute, rumVisitorsBreakdownRoute, rumWebCoreVitals, + rumUrlSearch, rumLongTaskMetrics, } from './rum_client'; import { @@ -173,6 +174,7 @@ const createApmApi = () => { .add(rumServicesRoute) .add(rumVisitorsBreakdownRoute) .add(rumWebCoreVitals) + .add(rumUrlSearch) .add(rumLongTaskMetrics) // Observability dashboard diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index 179279b6f2d8a..e3a846f9fb5c7 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -16,37 +16,54 @@ import { getRumServices } from '../lib/rum_client/get_rum_services'; import { getVisitorBreakdown } from '../lib/rum_client/get_visitor_breakdown'; import { getWebCoreVitals } from '../lib/rum_client/get_web_core_vitals'; import { getLongTaskMetrics } from '../lib/rum_client/get_long_task_metrics'; +import { getUrlSearch } from '../lib/rum_client/get_url_search'; export const percentileRangeRt = t.partial({ minPercentile: t.string, maxPercentile: t.string, }); +const urlQueryRt = t.partial({ urlQuery: t.string }); + export const rumClientMetricsRoute = createRoute(() => ({ path: '/api/apm/rum/client-metrics', params: { - query: t.intersection([uiFiltersRt, rangeRt]), + query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return getClientMetrics({ setup }); + const { + query: { urlQuery }, + } = context.params; + + return getClientMetrics({ setup, urlQuery }); }, })); export const rumPageLoadDistributionRoute = createRoute(() => ({ path: '/api/apm/rum-client/page-load-distribution', params: { - query: t.intersection([uiFiltersRt, rangeRt, percentileRangeRt]), + query: t.intersection([ + uiFiltersRt, + rangeRt, + percentileRangeRt, + urlQueryRt, + ]), }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { - query: { minPercentile, maxPercentile }, + query: { minPercentile, maxPercentile, urlQuery }, } = context.params; - return getPageLoadDistribution({ setup, minPercentile, maxPercentile }); + return getPageLoadDistribution({ + setup, + minPercentile, + maxPercentile, + urlQuery, + }); }, })); @@ -57,6 +74,7 @@ export const rumPageLoadDistBreakdownRoute = createRoute(() => ({ uiFiltersRt, rangeRt, percentileRangeRt, + urlQueryRt, t.type({ breakdown: t.string }), ]), }, @@ -64,7 +82,7 @@ export const rumPageLoadDistBreakdownRoute = createRoute(() => ({ const setup = await setupRequest(context, request); const { - query: { minPercentile, maxPercentile, breakdown }, + query: { minPercentile, maxPercentile, breakdown, urlQuery }, } = context.params; return getPageLoadDistBreakdown({ @@ -72,6 +90,7 @@ export const rumPageLoadDistBreakdownRoute = createRoute(() => ({ minDuration: Number(minPercentile), maxDuration: Number(maxPercentile), breakdown, + urlQuery, }); }, })); @@ -82,6 +101,7 @@ export const rumPageViewsTrendRoute = createRoute(() => ({ query: t.intersection([ uiFiltersRt, rangeRt, + urlQueryRt, t.partial({ breakdowns: t.string }), ]), }, @@ -89,10 +109,10 @@ export const rumPageViewsTrendRoute = createRoute(() => ({ const setup = await setupRequest(context, request); const { - query: { breakdowns }, + query: { breakdowns, urlQuery }, } = context.params; - return getPageViewTrends({ setup, breakdowns }); + return getPageViewTrends({ setup, breakdowns, urlQuery }); }, })); @@ -111,35 +131,63 @@ export const rumServicesRoute = createRoute(() => ({ export const rumVisitorsBreakdownRoute = createRoute(() => ({ path: '/api/apm/rum-client/visitor-breakdown', params: { - query: t.intersection([uiFiltersRt, rangeRt]), + query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return getVisitorBreakdown({ setup }); + const { + query: { urlQuery }, + } = context.params; + + return getVisitorBreakdown({ setup, urlQuery }); }, })); export const rumWebCoreVitals = createRoute(() => ({ path: '/api/apm/rum-client/web-core-vitals', params: { - query: t.intersection([uiFiltersRt, rangeRt]), + query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return getWebCoreVitals({ setup }); + const { + query: { urlQuery }, + } = context.params; + + return getWebCoreVitals({ setup, urlQuery }); }, })); export const rumLongTaskMetrics = createRoute(() => ({ path: '/api/apm/rum-client/long-task-metrics', params: { - query: t.intersection([uiFiltersRt, rangeRt]), + query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return getLongTaskMetrics({ setup }); + const { + query: { urlQuery }, + } = context.params; + + return getLongTaskMetrics({ setup, urlQuery }); + }, +})); + +export const rumUrlSearch = createRoute(() => ({ + path: '/api/apm/rum-client/url-search', + params: { + query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const { + query: { urlQuery }, + } = context.params; + + return getUrlSearch({ setup, urlQuery }); }, })); diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts b/x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts new file mode 100644 index 0000000000000..76dc758895e32 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function rumServicesApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('CSM url search api', () => { + describe('when there is no data', () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/rum-client/url-search?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D' + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatchInline(` + Object { + "items": Array [], + "total": 0, + } + `); + }); + }); + + describe('when there is data', () => { + before(async () => { + await esArchiver.load('8.0.0'); + await esArchiver.load('rum_8.0.0'); + }); + after(async () => { + await esArchiver.unload('8.0.0'); + await esArchiver.unload('rum_8.0.0'); + }); + + it('returns top urls when no query', async () => { + const response = await supertest.get( + '/api/apm/rum-client/url-search?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(` + Object { + "items": Array [ + Object { + "count": 5, + "pld": 4924000, + "url": "http://localhost:5601/nfw/app/csm?rangeFrom=now-15m&rangeTo=now&serviceName=kibana-frontend-8_0_0", + }, + Object { + "count": 1, + "pld": 2760000, + "url": "http://localhost:5601/nfw/app/home", + }, + ], + "total": 2, + } + `); + }); + + it('returns specific results against query', async () => { + const response = await supertest.get( + '/api/apm/rum-client/url-search?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D&urlQuery=csm' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(` + Object { + "items": Array [ + Object { + "count": 5, + "pld": 4924000, + "url": "http://localhost:5601/nfw/app/csm?rangeFrom=now-15m&rangeTo=now&serviceName=kibana-frontend-8_0_0", + }, + ], + "total": 1, + } + `); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index a026f91a02cd7..69e54ea33c559 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -35,6 +35,7 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr loadTestFile(require.resolve('./csm/csm_services.ts')); loadTestFile(require.resolve('./csm/web_core_vitals.ts')); loadTestFile(require.resolve('./csm/long_task_metrics.ts')); + loadTestFile(require.resolve('./csm/url_search.ts')); loadTestFile(require.resolve('./csm/page_views.ts')); }); }); From 75a84ad56fee9ba4c7d79f7e41efe8778e1c44bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Wed, 23 Sep 2020 12:50:07 +0100 Subject: [PATCH 37/92] [Lens] Rename "telemetry" to "stats" (#78125) --- x-pack/plugins/lens/public/lens_ui_telemetry/factory.test.ts | 2 +- x-pack/plugins/lens/public/lens_ui_telemetry/factory.ts | 2 +- x-pack/plugins/lens/server/routes/telemetry.ts | 2 +- x-pack/test/api_integration/apis/lens/telemetry.ts | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/lens/public/lens_ui_telemetry/factory.test.ts b/x-pack/plugins/lens/public/lens_ui_telemetry/factory.test.ts index 8bb1e086a37c2..fa7747dd18e42 100644 --- a/x-pack/plugins/lens/public/lens_ui_telemetry/factory.test.ts +++ b/x-pack/plugins/lens/public/lens_ui_telemetry/factory.test.ts @@ -83,7 +83,7 @@ describe('Lens UI telemetry', () => { jest.runOnlyPendingTimers(); - expect(http.post).toHaveBeenCalledWith(`/api/lens/telemetry`, { + expect(http.post).toHaveBeenCalledWith(`/api/lens/stats`, { body: JSON.stringify({ events: { '2019-10-23': { diff --git a/x-pack/plugins/lens/public/lens_ui_telemetry/factory.ts b/x-pack/plugins/lens/public/lens_ui_telemetry/factory.ts index cb517acff4f7a..8f9ce7f2ceab8 100644 --- a/x-pack/plugins/lens/public/lens_ui_telemetry/factory.ts +++ b/x-pack/plugins/lens/public/lens_ui_telemetry/factory.ts @@ -86,7 +86,7 @@ export class LensReportManager { this.readFromStorage(); if (Object.keys(this.events).length || Object.keys(this.suggestionEvents).length) { try { - await this.http.post(`${BASE_API_URL}/telemetry`, { + await this.http.post(`${BASE_API_URL}/stats`, { body: JSON.stringify({ events: this.events, suggestionEvents: this.suggestionEvents, diff --git a/x-pack/plugins/lens/server/routes/telemetry.ts b/x-pack/plugins/lens/server/routes/telemetry.ts index 7925416ff5df2..06a7091104871 100644 --- a/x-pack/plugins/lens/server/routes/telemetry.ts +++ b/x-pack/plugins/lens/server/routes/telemetry.ts @@ -15,7 +15,7 @@ export async function initLensUsageRoute(setup: CoreSetup) { const router = setup.http.createRouter(); router.post( { - path: `${BASE_API_URL}/telemetry`, + path: `${BASE_API_URL}/stats`, validate: { body: schema.object({ events: schema.mapOf(schema.string(), schema.mapOf(schema.string(), schema.number())), diff --git a/x-pack/test/api_integration/apis/lens/telemetry.ts b/x-pack/test/api_integration/apis/lens/telemetry.ts index 0ae4753cd2967..5525a82b02ee8 100644 --- a/x-pack/test/api_integration/apis/lens/telemetry.ts +++ b/x-pack/test/api_integration/apis/lens/telemetry.ts @@ -60,7 +60,7 @@ export default ({ getService }: FtrProviderContext) => { it('should do nothing on empty post', async () => { await supertest - .post('/api/lens/telemetry') + .post('/api/lens/stats') .set(COMMON_HEADERS) .send({ events: {}, @@ -73,7 +73,7 @@ export default ({ getService }: FtrProviderContext) => { it('should write a document per results', async () => { await supertest - .post('/api/lens/telemetry') + .post('/api/lens/stats') .set(COMMON_HEADERS) .send({ events: { From 2c6cae7b9a3984f43f7c61c91eb26c5313d74911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Wed, 23 Sep 2020 14:12:49 +0200 Subject: [PATCH 38/92] [Ingest Pipelines] Add url generator for ingest pipelines app (#77872) * [Ingest Pipelines] Add url generator for ingest pipelines app * [Ingest Pipelines] Fix type check error * [Ingest Pipelines] Fix import errors * [Ingest Pipelines] Fix type check errors * [Ingest Pipelines] Fix type check errors * [ILM] Update UrlGenerator interface, clean up internal navigation service * [ILM] Fix function export * [ILM] Update functions signatures * [ILM] Fix errors * [ILM] Fix errors * [ILM] Rename ROUTES_CONFIG and export MANAGEMENT_APP_ID Co-authored-by: Elastic Machine --- src/plugins/management/common/contants.ts | 20 ++++ src/plugins/management/public/index.ts | 2 + src/plugins/management/public/plugin.ts | 3 +- .../helpers/pipelines_clone.helpers.ts | 6 +- .../helpers/pipelines_create.helpers.ts | 6 +- .../helpers/pipelines_edit.helpers.ts | 6 +- .../helpers/pipelines_list.helpers.ts | 6 +- .../ingest_pipelines/common/constants.ts | 4 +- x-pack/plugins/ingest_pipelines/kibana.json | 2 +- .../public/application/app.tsx | 9 +- .../pipelines_create/pipelines_create.tsx | 6 +- .../pipelines_edit/pipelines_edit.tsx | 8 +- .../sections/pipelines_list/empty_list.tsx | 7 +- .../sections/pipelines_list/main.tsx | 12 +- .../public/application/services/navigation.ts | 44 +++++++ .../plugins/ingest_pipelines/public/index.ts | 7 ++ .../plugins/ingest_pipelines/public/plugin.ts | 5 +- .../plugins/ingest_pipelines/public/types.ts | 2 + .../public/url_generator.test.ts | 107 ++++++++++++++++++ .../ingest_pipelines/public/url_generator.ts | 98 ++++++++++++++++ 20 files changed, 325 insertions(+), 35 deletions(-) create mode 100644 src/plugins/management/common/contants.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/services/navigation.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/url_generator.test.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/url_generator.ts diff --git a/src/plugins/management/common/contants.ts b/src/plugins/management/common/contants.ts new file mode 100644 index 0000000000000..6ff585510dab1 --- /dev/null +++ b/src/plugins/management/common/contants.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const MANAGEMENT_APP_ID = 'management'; diff --git a/src/plugins/management/public/index.ts b/src/plugins/management/public/index.ts index f6c23ccf0143f..f3e25b90b73c7 100644 --- a/src/plugins/management/public/index.ts +++ b/src/plugins/management/public/index.ts @@ -32,3 +32,5 @@ export { ManagementStart, DefinedSections, } from './types'; + +export { MANAGEMENT_APP_ID } from '../common/contants'; diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index 808578c470ae1..122e73796753c 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -33,6 +33,7 @@ import { AppNavLinkStatus, } from '../../../core/public'; +import { MANAGEMENT_APP_ID } from '../common/contants'; import { ManagementSectionsService, getSectionsServiceStartPrivate, @@ -72,7 +73,7 @@ export class ManagementPlugin implements Plugin & { actions: ReturnType; @@ -29,8 +29,8 @@ export const PIPELINE_TO_CLONE = { const testBedConfig: TestBedConfig = { memoryRouter: { - initialEntries: [`${BASE_PATH}create/${PIPELINE_TO_CLONE.name}`], - componentRoutePath: `${BASE_PATH}create/:name`, + initialEntries: [getClonePath({ clonedPipelineName: PIPELINE_TO_CLONE.name })], + componentRoutePath: ROUTES.clone, }, doMountAsync: true, }; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts index ce5ab1faa01be..22f68f12804d6 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts @@ -5,10 +5,10 @@ */ import { registerTestBed, TestBedConfig, TestBed } from '../../../../../test_utils'; -import { BASE_PATH } from '../../../common/constants'; import { PipelinesCreate } from '../../../public/application/sections/pipelines_create'; import { getFormActions, PipelineFormTestSubjects } from './pipeline_form.helpers'; import { WithAppDependencies } from './setup_environment'; +import { getCreatePath, ROUTES } from '../../../public/application/services/navigation'; export type PipelinesCreateTestBed = TestBed & { actions: ReturnType; @@ -16,8 +16,8 @@ export type PipelinesCreateTestBed = TestBed & { const testBedConfig: TestBedConfig = { memoryRouter: { - initialEntries: [`${BASE_PATH}/create`], - componentRoutePath: `${BASE_PATH}/create`, + initialEntries: [getCreatePath()], + componentRoutePath: ROUTES.create, }, doMountAsync: true, }; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts index 31c9630086178..5e0739f78eecd 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts @@ -5,10 +5,10 @@ */ import { registerTestBed, TestBedConfig, TestBed } from '../../../../../test_utils'; -import { BASE_PATH } from '../../../common/constants'; import { PipelinesEdit } from '../../../public/application/sections/pipelines_edit'; import { getFormActions, PipelineFormTestSubjects } from './pipeline_form.helpers'; import { WithAppDependencies } from './setup_environment'; +import { getEditPath, ROUTES } from '../../../public/application/services/navigation'; export type PipelinesEditTestBed = TestBed & { actions: ReturnType; @@ -29,8 +29,8 @@ export const PIPELINE_TO_EDIT = { const testBedConfig: TestBedConfig = { memoryRouter: { - initialEntries: [`${BASE_PATH}edit/${PIPELINE_TO_EDIT.name}`], - componentRoutePath: `${BASE_PATH}edit/:name`, + initialEntries: [getEditPath({ pipelineName: PIPELINE_TO_EDIT.name })], + componentRoutePath: ROUTES.edit, }, doMountAsync: true, }; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts index 03ffe361bb5a6..43ca849e61aee 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts @@ -6,7 +6,6 @@ import { act } from 'react-dom/test-utils'; -import { BASE_PATH } from '../../../common/constants'; import { registerTestBed, TestBed, @@ -16,11 +15,12 @@ import { } from '../../../../../test_utils'; import { PipelinesList } from '../../../public/application/sections/pipelines_list'; import { WithAppDependencies } from './setup_environment'; +import { getListPath, ROUTES } from '../../../public/application/services/navigation'; const testBedConfig: TestBedConfig = { memoryRouter: { - initialEntries: [BASE_PATH], - componentRoutePath: BASE_PATH, + initialEntries: [getListPath()], + componentRoutePath: ROUTES.list, }, doMountAsync: true, }; diff --git a/x-pack/plugins/ingest_pipelines/common/constants.ts b/x-pack/plugins/ingest_pipelines/common/constants.ts index 4c6c6fefaad83..0d6f977bfbfed 100644 --- a/x-pack/plugins/ingest_pipelines/common/constants.ts +++ b/x-pack/plugins/ingest_pipelines/common/constants.ts @@ -9,9 +9,9 @@ const basicLicense: LicenseType = 'basic'; export const PLUGIN_ID = 'ingest_pipelines'; -export const PLUGIN_MIN_LICENSE_TYPE = basicLicense; +export const MANAGEMENT_APP_ID = 'management'; -export const BASE_PATH = '/'; +export const PLUGIN_MIN_LICENSE_TYPE = basicLicense; export const API_BASE_PATH = '/api/ingest_pipelines'; diff --git a/x-pack/plugins/ingest_pipelines/kibana.json b/x-pack/plugins/ingest_pipelines/kibana.json index 38d28fbba20b4..2fe87c5e7a162 100644 --- a/x-pack/plugins/ingest_pipelines/kibana.json +++ b/x-pack/plugins/ingest_pipelines/kibana.json @@ -3,7 +3,7 @@ "version": "8.0.0", "server": true, "ui": true, - "requiredPlugins": ["licensing", "management", "features"], + "requiredPlugins": ["licensing", "management", "features", "share"], "optionalPlugins": ["security", "usageCollection"], "configPath": ["xpack", "ingest_pipelines"], "requiredBundles": ["esUiShared", "kibanaReact"] diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx index 55b59caab8d60..e78c4d3983183 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/app.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/app.tsx @@ -21,13 +21,14 @@ import { } from '../shared_imports'; import { PipelinesList, PipelinesCreate, PipelinesEdit, PipelinesClone } from './sections'; +import { ROUTES } from './services/navigation'; export const AppWithoutRouter = () => ( - - - - + + + + {/* Catch all */} diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx index acca1c4e03f40..d4aa11715248e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx @@ -16,7 +16,7 @@ import { EuiSpacer, } from '@elastic/eui'; -import { BASE_PATH } from '../../../../common/constants'; +import { getListPath } from '../../services/navigation'; import { Pipeline } from '../../../../common/types'; import { useKibana } from '../../../shared_imports'; import { PipelineForm } from '../../components'; @@ -50,11 +50,11 @@ export const PipelinesCreate: React.FunctionComponent { - history.push(BASE_PATH); + history.push(getListPath()); }; useEffect(() => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx index e09cf4820771f..35ca1635ab9c3 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx @@ -17,11 +17,11 @@ import { } from '@elastic/eui'; import { EuiCallOut } from '@elastic/eui'; -import { BASE_PATH } from '../../../../common/constants'; import { Pipeline } from '../../../../common/types'; import { useKibana, SectionLoading } from '../../../shared_imports'; -import { PipelineForm } from '../../components'; +import { getListPath } from '../../services/navigation'; +import { PipelineForm } from '../../components'; import { attemptToURIDecode } from '../shared'; interface MatchParams { @@ -56,11 +56,11 @@ export const PipelinesEdit: React.FunctionComponent { - history.push(BASE_PATH); + history.push(getListPath()); }; useEffect(() => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx index eba69ff454911..7f4caa09b6df0 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx @@ -11,6 +11,7 @@ import { useHistory } from 'react-router-dom'; import { ScopedHistory } from 'kibana/public'; import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; import { useKibana } from '../../../shared_imports'; +import { getCreatePath } from '../../services/navigation'; export const EmptyList: FunctionComponent = () => { const { services } = useKibana(); @@ -44,7 +45,11 @@ export const EmptyList: FunctionComponent = () => {

} actions={ - + {i18n.translate('xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel', { defaultMessage: 'Create a pipeline', })} diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx index 88148f1bc5746..be31f86e30c27 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx @@ -24,9 +24,9 @@ import { } from '@elastic/eui'; import { Pipeline } from '../../../../common/types'; -import { BASE_PATH } from '../../../../common/constants'; import { useKibana, SectionLoading } from '../../../shared_imports'; import { UIM_PIPELINES_LIST_LOAD } from '../../constants'; +import { getEditPath, getClonePath, getListPath } from '../../services/navigation'; import { EmptyList } from './empty_list'; import { PipelineTable } from './table'; @@ -67,17 +67,17 @@ export const PipelinesList: React.FunctionComponent = ({ } }, [pipelineNameFromLocation, data]); - const goToEditPipeline = (name: string) => { - history.push(`${BASE_PATH}/edit/${encodeURIComponent(name)}`); + const goToEditPipeline = (pipelineName: string) => { + history.push(getEditPath({ pipelineName })); }; - const goToClonePipeline = (name: string) => { - history.push(`${BASE_PATH}/create/${encodeURIComponent(name)}`); + const goToClonePipeline = (clonedPipelineName: string) => { + history.push(getClonePath({ clonedPipelineName })); }; const goHome = () => { setShowFlyout(false); - history.push(BASE_PATH); + history.push(getListPath()); }; if (data && data.length === 0) { diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/navigation.ts b/x-pack/plugins/ingest_pipelines/public/application/services/navigation.ts new file mode 100644 index 0000000000000..3ac3de6eac710 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/services/navigation.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const BASE_PATH = '/'; + +const EDIT_PATH = 'edit'; + +const CREATE_PATH = 'create'; + +const _getEditPath = (name: string, encode = true): string => { + return `${BASE_PATH}${EDIT_PATH}/${encode ? encodeURIComponent(name) : name}`; +}; + +const _getCreatePath = (): string => { + return `${BASE_PATH}${CREATE_PATH}`; +}; + +const _getClonePath = (name: string, encode = true): string => { + return `${BASE_PATH}${CREATE_PATH}/${encode ? encodeURIComponent(name) : name}`; +}; +const _getListPath = (name?: string): string => { + return `${BASE_PATH}${name ? `?pipeline=${encodeURIComponent(name)}` : ''}`; +}; + +export const ROUTES = { + list: _getListPath(), + edit: _getEditPath(':name', false), + create: _getCreatePath(), + clone: _getClonePath(':sourceName', false), +}; + +export const getListPath = ({ + inspectedPipelineName, +}: { + inspectedPipelineName?: string; +} = {}): string => _getListPath(inspectedPipelineName); +export const getEditPath = ({ pipelineName }: { pipelineName: string }): string => + _getEditPath(pipelineName, true); +export const getCreatePath = (): string => _getCreatePath(); +export const getClonePath = ({ clonedPipelineName }: { clonedPipelineName: string }): string => + _getClonePath(clonedPipelineName, true); diff --git a/x-pack/plugins/ingest_pipelines/public/index.ts b/x-pack/plugins/ingest_pipelines/public/index.ts index 7247973703804..637d4aad7264a 100644 --- a/x-pack/plugins/ingest_pipelines/public/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/index.ts @@ -9,3 +9,10 @@ import { IngestPipelinesPlugin } from './plugin'; export function plugin() { return new IngestPipelinesPlugin(); } + +export { + INGEST_PIPELINES_APP_ULR_GENERATOR, + IngestPipelinesUrlGenerator, + IngestPipelinesUrlGeneratorState, + INGEST_PIPELINES_PAGES, +} from './url_generator'; diff --git a/x-pack/plugins/ingest_pipelines/public/plugin.ts b/x-pack/plugins/ingest_pipelines/public/plugin.ts index 339068f185d1d..6c2f4a0898327 100644 --- a/x-pack/plugins/ingest_pipelines/public/plugin.ts +++ b/x-pack/plugins/ingest_pipelines/public/plugin.ts @@ -10,10 +10,11 @@ import { CoreSetup, Plugin } from 'src/core/public'; import { PLUGIN_ID } from '../common/constants'; import { uiMetricService, apiService } from './application/services'; import { Dependencies } from './types'; +import { registerUrlGenerator } from './url_generator'; export class IngestPipelinesPlugin implements Plugin { public setup(coreSetup: CoreSetup, plugins: Dependencies): void { - const { management, usageCollection } = plugins; + const { management, usageCollection, share } = plugins; const { http, getStartServices } = coreSetup; // Initialize services @@ -46,6 +47,8 @@ export class IngestPipelinesPlugin implements Plugin { }; }, }); + + registerUrlGenerator(coreSetup, management, share); } public start() {} diff --git a/x-pack/plugins/ingest_pipelines/public/types.ts b/x-pack/plugins/ingest_pipelines/public/types.ts index 91783ea04fa9a..e968c87226d07 100644 --- a/x-pack/plugins/ingest_pipelines/public/types.ts +++ b/x-pack/plugins/ingest_pipelines/public/types.ts @@ -6,8 +6,10 @@ import { ManagementSetup } from 'src/plugins/management/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; +import { SharePluginSetup } from '../../../../src/plugins/share/public'; export interface Dependencies { management: ManagementSetup; usageCollection: UsageCollectionSetup; + share: SharePluginSetup; } diff --git a/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts b/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts new file mode 100644 index 0000000000000..1267d526fb7d4 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IngestPipelinesUrlGenerator, INGEST_PIPELINES_PAGES } from './url_generator'; + +describe('IngestPipelinesUrlGenerator', () => { + const getAppBasePath = (absolute: boolean = false) => { + if (absolute) { + return Promise.resolve('http://localhost/app/test_app'); + } + return Promise.resolve('/app/test_app'); + }; + const urlGenerator = new IngestPipelinesUrlGenerator(getAppBasePath); + + describe('Pipelines List', () => { + it('generates relative url for list without pipelineId', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.LIST, + }); + expect(url).toBe('/app/test_app/'); + }); + + it('generates absolute url for list without pipelineId', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.LIST, + absolute: true, + }); + expect(url).toBe('http://localhost/app/test_app/'); + }); + it('generates relative url for list with a pipelineId', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.LIST, + pipelineId: 'pipeline_name', + }); + expect(url).toBe('/app/test_app/?pipeline=pipeline_name'); + }); + + it('generates absolute url for list with a pipelineId', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.LIST, + pipelineId: 'pipeline_name', + absolute: true, + }); + expect(url).toBe('http://localhost/app/test_app/?pipeline=pipeline_name'); + }); + }); + + describe('Pipeline Edit', () => { + it('generates relative url for pipeline edit', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.EDIT, + pipelineId: 'pipeline_name', + }); + expect(url).toBe('/app/test_app/edit/pipeline_name'); + }); + + it('generates absolute url for pipeline edit', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.EDIT, + pipelineId: 'pipeline_name', + absolute: true, + }); + expect(url).toBe('http://localhost/app/test_app/edit/pipeline_name'); + }); + }); + + describe('Pipeline Clone', () => { + it('generates relative url for pipeline clone', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.CLONE, + pipelineId: 'pipeline_name', + }); + expect(url).toBe('/app/test_app/create/pipeline_name'); + }); + + it('generates absolute url for pipeline clone', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.CLONE, + pipelineId: 'pipeline_name', + absolute: true, + }); + expect(url).toBe('http://localhost/app/test_app/create/pipeline_name'); + }); + }); + + describe('Pipeline Create', () => { + it('generates relative url for pipeline create', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.CREATE, + pipelineId: 'pipeline_name', + }); + expect(url).toBe('/app/test_app/create'); + }); + + it('generates absolute url for pipeline create', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.CREATE, + pipelineId: 'pipeline_name', + absolute: true, + }); + expect(url).toBe('http://localhost/app/test_app/create'); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/url_generator.ts b/x-pack/plugins/ingest_pipelines/public/url_generator.ts new file mode 100644 index 0000000000000..043d449a0440a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/url_generator.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from 'src/core/public'; +import { MANAGEMENT_APP_ID } from '../../../../src/plugins/management/public'; +import { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public'; +import { + getClonePath, + getCreatePath, + getEditPath, + getListPath, +} from './application/services/navigation'; +import { Dependencies } from './types'; +import { PLUGIN_ID } from '../common/constants'; + +export const INGEST_PIPELINES_APP_ULR_GENERATOR = 'INGEST_PIPELINES_APP_URL_GENERATOR'; + +export enum INGEST_PIPELINES_PAGES { + LIST = 'pipelines_list', + EDIT = 'pipeline_edit', + CREATE = 'pipeline_create', + CLONE = 'pipeline_clone', +} + +interface UrlGeneratorState { + pipelineId: string; + absolute?: boolean; +} +export interface PipelinesListUrlGeneratorState extends Partial { + page: INGEST_PIPELINES_PAGES.LIST; +} + +export interface PipelineEditUrlGeneratorState extends UrlGeneratorState { + page: INGEST_PIPELINES_PAGES.EDIT; +} + +export interface PipelineCloneUrlGeneratorState extends UrlGeneratorState { + page: INGEST_PIPELINES_PAGES.CLONE; +} + +export interface PipelineCreateUrlGeneratorState extends UrlGeneratorState { + page: INGEST_PIPELINES_PAGES.CREATE; +} + +export type IngestPipelinesUrlGeneratorState = + | PipelinesListUrlGeneratorState + | PipelineEditUrlGeneratorState + | PipelineCloneUrlGeneratorState + | PipelineCreateUrlGeneratorState; + +export class IngestPipelinesUrlGenerator + implements UrlGeneratorsDefinition { + constructor(private readonly getAppBasePath: (absolute: boolean) => Promise) {} + + public readonly id = INGEST_PIPELINES_APP_ULR_GENERATOR; + + public readonly createUrl = async (state: IngestPipelinesUrlGeneratorState): Promise => { + switch (state.page) { + case INGEST_PIPELINES_PAGES.EDIT: { + return `${await this.getAppBasePath(!!state.absolute)}${getEditPath({ + pipelineName: state.pipelineId, + })}`; + } + case INGEST_PIPELINES_PAGES.CREATE: { + return `${await this.getAppBasePath(!!state.absolute)}${getCreatePath()}`; + } + case INGEST_PIPELINES_PAGES.LIST: { + return `${await this.getAppBasePath(!!state.absolute)}${getListPath({ + inspectedPipelineName: state.pipelineId, + })}`; + } + case INGEST_PIPELINES_PAGES.CLONE: { + return `${await this.getAppBasePath(!!state.absolute)}${getClonePath({ + clonedPipelineName: state.pipelineId, + })}`; + } + } + }; +} + +export const registerUrlGenerator = ( + coreSetup: CoreSetup, + management: Dependencies['management'], + share: Dependencies['share'] +) => { + const getAppBasePath = async (absolute = false) => { + const [coreStart] = await coreSetup.getStartServices(); + return coreStart.application.getUrlForApp(MANAGEMENT_APP_ID, { + path: management.sections.section.ingest.getApp(PLUGIN_ID)!.basePath, + absolute: !!absolute, + }); + }; + + share.urlGenerators.registerUrlGenerator(new IngestPipelinesUrlGenerator(getAppBasePath)); +}; From 0ed3a5f303cfd5a81f12d8ca7a2d3519ad1ed5f3 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Wed, 23 Sep 2020 08:21:33 -0400 Subject: [PATCH 39/92] skip tests for old pacakge (#78194) --- .../apps/endpoint/endpoint_list.ts | 6 +++--- .../apis/artifacts/index.ts | 2 +- .../security_solution_endpoint_api_int/apis/metadata.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index 4beb64affc46b..d46171bbaa49f 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -86,7 +86,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.exists('emptyPolicyTable'); }); - it('finds data after load and polling', async () => { + it.skip('finds data after load and polling', async () => { await esArchiver.load('endpoint/metadata/destination_index', { useCreate: true }); await pageObjects.endpoint.waitForTableToHaveData('endpointListTable', 1100); const tableData = await pageObjects.endpointPageUtils.tableData('endpointListTable'); @@ -94,7 +94,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - describe('when there is data,', () => { + describe.skip('when there is data,', () => { before(async () => { await esArchiver.load('endpoint/metadata/destination_index', { useCreate: true }); await pageObjects.endpoint.navigateToEndpointList(); @@ -212,7 +212,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - describe('displays the correct table data for the kql queries', () => { + describe.skip('displays the correct table data for the kql queries', () => { before(async () => { await esArchiver.load('endpoint/metadata/destination_index', { useCreate: true }); await pageObjects.endpoint.navigateToEndpointList(); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts index 17a4182fe9371..6c225dea5430f 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts @@ -18,7 +18,7 @@ export default function (providerContext: FtrProviderContext) { const supertestWithoutAuth = getSupertestWithoutAuth(providerContext); let agentAccessAPIKey: string; - describe('artifact download', () => { + describe.skip('artifact download', () => { before(async () => { await esArchiver.load('endpoint/artifacts/api_feature', { useCreate: true }); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index b157c3159ccc0..d1e98876596e5 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -23,7 +23,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - describe('test metadata api', () => { + describe.skip('test metadata api', () => { describe(`POST ${METADATA_REQUEST_ROUTE} when index is empty`, () => { it('metadata api should return empty result when index is empty', async () => { await deleteMetadataStream(getService); From e26d35ebf2ce9826bbcf11a3728894618b160071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Wed, 23 Sep 2020 14:47:10 +0200 Subject: [PATCH 40/92] [Security Solution] Cleanup Tls graphql (#78265) --- .../public/graphql/introspection.json | 329 ------------ .../security_solution/public/graphql/types.ts | 365 ++++--------- .../network/components/tls_table/columns.tsx | 14 +- .../network/components/tls_table/mock.ts | 5 +- .../network/containers/tls/index.gql_query.ts | 57 --- .../public/network/containers/tls/index.tsx | 5 +- .../security_solution/server/graphql/index.ts | 2 - .../server/graphql/tls/index.ts | 8 - .../server/graphql/tls/resolvers.ts | 40 -- .../server/graphql/tls/schema.gql.ts | 47 -- .../security_solution/server/graphql/types.ts | 187 ------- .../security_solution/server/init_server.ts | 2 - .../server/lib/compose/kibana.ts | 2 - .../lib/tls/elasticsearch_adapter.test.ts | 63 --- .../server/lib/tls/elasticsearch_adapter.ts | 82 --- .../security_solution/server/lib/tls/index.ts | 26 - .../security_solution/server/lib/tls/mock.ts | 481 ------------------ .../server/lib/tls/query_tls.dsl.ts | 107 ---- .../security_solution/server/lib/tls/types.ts | 36 -- .../security_solution/server/lib/types.ts | 2 - .../apis/security_solution/index.js | 2 +- .../apis/security_solution/tls.ts | 3 + 22 files changed, 127 insertions(+), 1738 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/network/containers/tls/index.gql_query.ts delete mode 100644 x-pack/plugins/security_solution/server/graphql/tls/index.ts delete mode 100644 x-pack/plugins/security_solution/server/graphql/tls/resolvers.ts delete mode 100644 x-pack/plugins/security_solution/server/graphql/tls/schema.gql.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/tls/elasticsearch_adapter.test.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/tls/elasticsearch_adapter.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/tls/index.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/tls/mock.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/tls/types.ts diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index b32083fec1b5e..0bbc1fcc80e92 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -2186,103 +2186,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "Tls", - "description": "", - "args": [ - { - "name": "filterQuery", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "id", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "ip", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "pagination", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "PaginationInputPaginated", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "sort", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "TlsSortField", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "flowTarget", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "ENUM", "name": "FlowTargetSourceDest", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "timerange", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "defaultIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "TlsData", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "UncommonProcesses", "description": "Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified", @@ -9444,238 +9347,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "INPUT_OBJECT", - "name": "TlsSortField", - "description": "", - "fields": null, - "inputFields": [ - { - "name": "field", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "ENUM", "name": "TlsFields", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "direction", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "ENUM", "name": "Direction", "ofType": null } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "TlsFields", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { "name": "_id", "description": "", "isDeprecated": false, "deprecationReason": null } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "TlsData", - "description": "", - "fields": [ - { - "name": "edges", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "TlsEdges", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "totalCount", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "PageInfoPaginated", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "TlsEdges", - "description": "", - "fields": [ - { - "name": "node", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "TlsNode", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "cursor", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "CursorType", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "TlsNode", - "description": "", - "fields": [ - { - "name": "_id", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "timestamp", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "notAfter", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "subjects", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "ja3", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "issuers", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "UncommonProcessesData", diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 65d9212f77dcc..4d3837f434b05 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -95,12 +95,6 @@ export interface NetworkHttpSortField { direction: Direction; } -export interface TlsSortField { - field: TlsFields; - - direction: Direction; -} - export interface PageInfoTimeline { pageIndex: number; @@ -354,10 +348,6 @@ export enum NetworkDnsFields { dnsBytesOut = 'dnsBytesOut', } -export enum TlsFields { - _id = '_id', -} - export enum DataProviderType { default = 'default', template = 'template', @@ -568,8 +558,6 @@ export interface Source { OverviewNetwork?: Maybe; OverviewHost?: Maybe; - - Tls: TlsData; /** Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified */ UncommonProcesses: UncommonProcessesData; /** Just a simple example to get the app name */ @@ -1928,36 +1916,6 @@ export interface OverviewHostData { inspect?: Maybe; } -export interface TlsData { - edges: TlsEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe; -} - -export interface TlsEdges { - node: TlsNode; - - cursor: CursorType; -} - -export interface TlsNode { - _id?: Maybe; - - timestamp?: Maybe; - - notAfter?: Maybe; - - subjects?: Maybe; - - ja3?: Maybe; - - issuers?: Maybe; -} - export interface UncommonProcessesData { edges: UncommonProcessesEdges[]; @@ -2573,23 +2531,6 @@ export interface OverviewHostSourceArgs { defaultIndex: string[]; } -export interface TlsSourceArgs { - filterQuery?: Maybe; - - id?: Maybe; - - ip: string; - - pagination: PaginationInputPaginated; - - sort: TlsSortField; - - flowTarget: FlowTargetSourceDest; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} export interface UncommonProcessesSourceArgs { timerange: TimerangeInput; @@ -2930,6 +2871,116 @@ export namespace GetAuthenticationsQuery { }; } +export namespace GetHostOverviewQuery { + export type Variables = { + sourceId: string; + hostName: string; + timerange: TimerangeInput; + defaultIndex: string[]; + inspect: boolean; + }; + + export type Query = { + __typename?: 'Query'; + + source: Source; + }; + + export type Source = { + __typename?: 'Source'; + + id: string; + + HostOverview: HostOverview; + }; + + export type HostOverview = { + __typename?: 'HostItem'; + + _id: Maybe; + + host: Maybe; + + cloud: Maybe; + + inspect: Maybe; + + endpoint: Maybe; + }; + + export type Host = { + __typename?: 'HostEcsFields'; + + architecture: Maybe; + + id: Maybe; + + ip: Maybe; + + mac: Maybe; + + name: Maybe; + + os: Maybe; + + type: Maybe; + }; + + export type Os = { + __typename?: 'OsEcsFields'; + + family: Maybe; + + name: Maybe; + + platform: Maybe; + + version: Maybe; + }; + + export type Cloud = { + __typename?: 'CloudFields'; + + instance: Maybe; + + machine: Maybe; + + provider: Maybe<(Maybe)[]>; + + region: Maybe<(Maybe)[]>; + }; + + export type Instance = { + __typename?: 'CloudInstance'; + + id: Maybe<(Maybe)[]>; + }; + + export type Machine = { + __typename?: 'CloudMachine'; + + type: Maybe<(Maybe)[]>; + }; + + export type Inspect = { + __typename?: 'Inspect'; + + dsl: string[]; + + response: string[]; + }; + + export type Endpoint = { + __typename?: 'EndpointFields'; + + endpointPolicy: Maybe; + + policyStatus: Maybe; + + sensorVersion: Maybe; + }; +} + export namespace GetHostFirstLastSeenQuery { export type Variables = { sourceId: string; @@ -3060,116 +3111,6 @@ export namespace GetHostsTableQuery { }; } -export namespace GetHostOverviewQuery { - export type Variables = { - sourceId: string; - hostName: string; - timerange: TimerangeInput; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - HostOverview: HostOverview; - }; - - export type HostOverview = { - __typename?: 'HostItem'; - - _id: Maybe; - - host: Maybe; - - cloud: Maybe; - - inspect: Maybe; - - endpoint: Maybe; - }; - - export type Host = { - __typename?: 'HostEcsFields'; - - architecture: Maybe; - - id: Maybe; - - ip: Maybe; - - mac: Maybe; - - name: Maybe; - - os: Maybe; - - type: Maybe; - }; - - export type Os = { - __typename?: 'OsEcsFields'; - - family: Maybe; - - name: Maybe; - - platform: Maybe; - - version: Maybe; - }; - - export type Cloud = { - __typename?: 'CloudFields'; - - instance: Maybe; - - machine: Maybe; - - provider: Maybe<(Maybe)[]>; - - region: Maybe<(Maybe)[]>; - }; - - export type Instance = { - __typename?: 'CloudInstance'; - - id: Maybe<(Maybe)[]>; - }; - - export type Machine = { - __typename?: 'CloudMachine'; - - type: Maybe<(Maybe)[]>; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; - - export type Endpoint = { - __typename?: 'EndpointFields'; - - endpointPolicy: Maybe; - - policyStatus: Maybe; - - sensorVersion: Maybe; - }; -} - export namespace GetKpiHostDetailsQuery { export type Variables = { sourceId: string; @@ -4119,92 +4060,6 @@ export namespace GetNetworkTopNFlowQuery { }; } -export namespace GetTlsQuery { - export type Variables = { - sourceId: string; - filterQuery?: Maybe; - flowTarget: FlowTargetSourceDest; - ip: string; - pagination: PaginationInputPaginated; - sort: TlsSortField; - timerange: TimerangeInput; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - Tls: Tls; - }; - - export type Tls = { - __typename?: 'TlsData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe; - }; - - export type Edges = { - __typename?: 'TlsEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'TlsNode'; - - _id: Maybe; - - subjects: Maybe; - - ja3: Maybe; - - issuers: Maybe; - - notAfter: Maybe; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - export namespace GetUsersQuery { export type Variables = { sourceId: string; diff --git a/x-pack/plugins/security_solution/public/network/components/tls_table/columns.tsx b/x-pack/plugins/security_solution/public/network/components/tls_table/columns.tsx index 33667a65a95e9..94de71017d339 100644 --- a/x-pack/plugins/security_solution/public/network/components/tls_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/network/components/tls_table/columns.tsx @@ -8,9 +8,9 @@ import React from 'react'; import moment from 'moment'; -import { TlsNode } from '../../../graphql/types'; -import { Columns } from '../../../common/components/paginated_table'; +import { NetworkTlsNode } from '../../../../common/search_strategy'; +import { Columns } from '../../../common/components/paginated_table'; import { getRowItemDraggables, getRowItemDraggable, @@ -21,11 +21,11 @@ import { PreferenceFormattedDate } from '../../../common/components/formatted_da import * as i18n from './translations'; export type TlsColumns = [ - Columns, - Columns, - Columns, - Columns, - Columns + Columns, + Columns, + Columns, + Columns, + Columns ]; export const getTlsColumns = (tableId: string): TlsColumns => [ diff --git a/x-pack/plugins/security_solution/public/network/components/tls_table/mock.ts b/x-pack/plugins/security_solution/public/network/components/tls_table/mock.ts index a90907eb38854..0e16d76d300de 100644 --- a/x-pack/plugins/security_solution/public/network/components/tls_table/mock.ts +++ b/x-pack/plugins/security_solution/public/network/components/tls_table/mock.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TlsData } from '../../../graphql/types'; +import { NetworkTlsStrategyResponse } from '../../../../common/search_strategy'; -export const mockTlsData: TlsData = { +export const mockTlsData: NetworkTlsStrategyResponse = { totalCount: 2, edges: [ { @@ -51,4 +51,5 @@ export const mockTlsData: TlsData = { fakeTotalCount: 50, showMorePagesIndicator: true, }, + rawResponse: {} as NetworkTlsStrategyResponse['rawResponse'], }; diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/index.gql_query.ts b/x-pack/plugins/security_solution/public/network/containers/tls/index.gql_query.ts deleted file mode 100644 index f513a94d69667..0000000000000 --- a/x-pack/plugins/security_solution/public/network/containers/tls/index.gql_query.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const tlsQuery = gql` - query GetTlsQuery( - $sourceId: ID! - $filterQuery: String - $flowTarget: FlowTargetSourceDest! - $ip: String! - $pagination: PaginationInputPaginated! - $sort: TlsSortField! - $timerange: TimerangeInput! - $defaultIndex: [String!]! - $inspect: Boolean! - ) { - source(id: $sourceId) { - id - Tls( - filterQuery: $filterQuery - flowTarget: $flowTarget - ip: $ip - pagination: $pagination - sort: $sort - timerange: $timerange - defaultIndex: $defaultIndex - ) { - totalCount - edges { - node { - _id - subjects - ja3 - issuers - notAfter - } - cursor { - value - } - } - pageInfo { - activePage - fakeTotalCount - showMorePagesIndicator - } - inspect @include(if: $inspect) { - dsl - response - } - } - } - } -`; diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx index f9393cfc26692..4c9658aa9b42c 100644 --- a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx @@ -14,7 +14,7 @@ import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { inputsModel, State } from '../../../common/store'; import { useKibana } from '../../../common/lib/kibana'; import { createFilter } from '../../../common/containers/helpers'; -import { TlsEdges, PageInfoPaginated, FlowTargetSourceDest } from '../../../graphql/types'; +import { PageInfoPaginated, FlowTargetSourceDest } from '../../../graphql/types'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; import { networkModel, networkSelectors } from '../../store'; import { @@ -40,7 +40,7 @@ export interface NetworkTlsArgs { loadPage: (newActivePage: number) => void; pageInfo: PageInfoPaginated; refetch: inputsModel.Refetch; - tls: TlsEdges[]; + tls: NetworkTlsStrategyResponse['edges']; totalCount: number; } @@ -81,6 +81,7 @@ export const useNetworkTls = ({ factoryQueryType: NetworkQueries.tls, filterQuery: createFilter(filterQuery), flowTarget, + id, ip, pagination: generateTablePaginationOptions(activePage, limit), sort, diff --git a/x-pack/plugins/security_solution/server/graphql/index.ts b/x-pack/plugins/security_solution/server/graphql/index.ts index 7e25735707893..959aa4549d43f 100644 --- a/x-pack/plugins/security_solution/server/graphql/index.ts +++ b/x-pack/plugins/security_solution/server/graphql/index.ts @@ -26,7 +26,6 @@ import { toNumberSchema } from './scalar_to_number_array'; import { sourceStatusSchema } from './source_status'; import { sourcesSchema } from './sources'; import { timelineSchema } from './timeline'; -import { tlsSchema } from './tls'; import { uncommonProcessesSchema } from './uncommon_processes'; import { whoAmISchema } from './who_am_i'; import { matrixHistogramSchema } from './matrix_histogram'; @@ -53,7 +52,6 @@ export const schemas = [ sourceStatusSchema, sharedSchema, timelineSchema, - tlsSchema, uncommonProcessesSchema, whoAmISchema, ]; diff --git a/x-pack/plugins/security_solution/server/graphql/tls/index.ts b/x-pack/plugins/security_solution/server/graphql/tls/index.ts deleted file mode 100644 index 7d745742090a6..0000000000000 --- a/x-pack/plugins/security_solution/server/graphql/tls/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { createTlsResolvers } from './resolvers'; -export { tlsSchema } from './schema.gql'; diff --git a/x-pack/plugins/security_solution/server/graphql/tls/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/tls/resolvers.ts deleted file mode 100644 index bfa3fddc3c8a5..0000000000000 --- a/x-pack/plugins/security_solution/server/graphql/tls/resolvers.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SourceResolvers } from '../../graphql/types'; -import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; -import { TLS, TlsRequestOptions } from '../../lib/tls'; -import { createOptionsPaginated } from '../../utils/build_query/create_options'; -import { QuerySourceResolver } from '../sources/resolvers'; - -export type QueryTlsResolver = ChildResolverOf< - AppResolverOf, - QuerySourceResolver ->; - -export interface TlsResolversDeps { - tls: TLS; -} - -export const createTlsResolvers = ( - libs: TlsResolversDeps -): { - Source: { - Tls: QueryTlsResolver; - }; -} => ({ - Source: { - async Tls(source, args, { req }, info) { - const options: TlsRequestOptions = { - ...createOptionsPaginated(source, args, info), - ip: args.ip, - sort: args.sort, - flowTarget: args.flowTarget, - }; - return libs.tls.getTls(req, options); - }, - }, -}); diff --git a/x-pack/plugins/security_solution/server/graphql/tls/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/tls/schema.gql.ts deleted file mode 100644 index 452c615c65aa5..0000000000000 --- a/x-pack/plugins/security_solution/server/graphql/tls/schema.gql.ts +++ /dev/null @@ -1,47 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const tlsSchema = gql` - enum TlsFields { - _id - } - type TlsNode { - _id: String - timestamp: Date - notAfter: [String!] - subjects: [String!] - ja3: [String!] - issuers: [String!] - } - input TlsSortField { - field: TlsFields! - direction: Direction! - } - type TlsEdges { - node: TlsNode! - cursor: CursorType! - } - type TlsData { - edges: [TlsEdges!]! - totalCount: Float! - pageInfo: PageInfoPaginated! - inspect: Inspect - } - extend type Source { - Tls( - filterQuery: String - id: String - ip: String! - pagination: PaginationInputPaginated! - sort: TlsSortField! - flowTarget: FlowTargetSourceDest! - timerange: TimerangeInput! - defaultIndex: [String!]! - ): TlsData! - } -`; diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 7638ebd03f6b1..ed3abd25df882 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -97,12 +97,6 @@ export interface NetworkHttpSortField { direction: Direction; } -export interface TlsSortField { - field: TlsFields; - - direction: Direction; -} - export interface PageInfoTimeline { pageIndex: number; @@ -356,10 +350,6 @@ export enum NetworkDnsFields { dnsBytesOut = 'dnsBytesOut', } -export enum TlsFields { - _id = '_id', -} - export enum DataProviderType { default = 'default', template = 'template', @@ -570,8 +560,6 @@ export interface Source { OverviewNetwork?: Maybe; OverviewHost?: Maybe; - - Tls: TlsData; /** Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified */ UncommonProcesses: UncommonProcessesData; /** Just a simple example to get the app name */ @@ -1930,36 +1918,6 @@ export interface OverviewHostData { inspect?: Maybe; } -export interface TlsData { - edges: TlsEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe; -} - -export interface TlsEdges { - node: TlsNode; - - cursor: CursorType; -} - -export interface TlsNode { - _id?: Maybe; - - timestamp?: Maybe; - - notAfter?: Maybe; - - subjects?: Maybe; - - ja3?: Maybe; - - issuers?: Maybe; -} - export interface UncommonProcessesData { edges: UncommonProcessesEdges[]; @@ -2575,23 +2533,6 @@ export interface OverviewHostSourceArgs { defaultIndex: string[]; } -export interface TlsSourceArgs { - filterQuery?: Maybe; - - id?: Maybe; - - ip: string; - - pagination: PaginationInputPaginated; - - sort: TlsSortField; - - flowTarget: FlowTargetSourceDest; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} export interface UncommonProcessesSourceArgs { timerange: TimerangeInput; @@ -3041,8 +2982,6 @@ export namespace SourceResolvers { OverviewNetwork?: OverviewNetworkResolver, TypeParent, TContext>; OverviewHost?: OverviewHostResolver, TypeParent, TContext>; - - Tls?: TlsResolver; /** Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified */ UncommonProcesses?: UncommonProcessesResolver; /** Just a simple example to get the app name */ @@ -3426,30 +3365,6 @@ export namespace SourceResolvers { defaultIndex: string[]; } - export type TlsResolver = Resolver< - R, - Parent, - TContext, - TlsArgs - >; - export interface TlsArgs { - filterQuery?: Maybe; - - id?: Maybe; - - ip: string; - - pagination: PaginationInputPaginated; - - sort: TlsSortField; - - flowTarget: FlowTargetSourceDest; - - timerange: TimerangeInput; - - defaultIndex: string[]; - } - export type UncommonProcessesResolver< R = UncommonProcessesData, Parent = Source, @@ -8021,105 +7936,6 @@ export namespace OverviewHostDataResolvers { > = Resolver; } -export namespace TlsDataResolvers { - export interface Resolvers { - edges?: EdgesResolver; - - totalCount?: TotalCountResolver; - - pageInfo?: PageInfoResolver; - - inspect?: InspectResolver, TypeParent, TContext>; - } - - export type EdgesResolver = Resolver< - R, - Parent, - TContext - >; - export type TotalCountResolver = Resolver< - R, - Parent, - TContext - >; - export type PageInfoResolver< - R = PageInfoPaginated, - Parent = TlsData, - TContext = SiemContext - > = Resolver; - export type InspectResolver< - R = Maybe, - Parent = TlsData, - TContext = SiemContext - > = Resolver; -} - -export namespace TlsEdgesResolvers { - export interface Resolvers { - node?: NodeResolver; - - cursor?: CursorResolver; - } - - export type NodeResolver = Resolver< - R, - Parent, - TContext - >; - export type CursorResolver = Resolver< - R, - Parent, - TContext - >; -} - -export namespace TlsNodeResolvers { - export interface Resolvers { - _id?: _IdResolver, TypeParent, TContext>; - - timestamp?: TimestampResolver, TypeParent, TContext>; - - notAfter?: NotAfterResolver, TypeParent, TContext>; - - subjects?: SubjectsResolver, TypeParent, TContext>; - - ja3?: Ja3Resolver, TypeParent, TContext>; - - issuers?: IssuersResolver, TypeParent, TContext>; - } - - export type _IdResolver, Parent = TlsNode, TContext = SiemContext> = Resolver< - R, - Parent, - TContext - >; - export type TimestampResolver< - R = Maybe, - Parent = TlsNode, - TContext = SiemContext - > = Resolver; - export type NotAfterResolver< - R = Maybe, - Parent = TlsNode, - TContext = SiemContext - > = Resolver; - export type SubjectsResolver< - R = Maybe, - Parent = TlsNode, - TContext = SiemContext - > = Resolver; - export type Ja3Resolver, Parent = TlsNode, TContext = SiemContext> = Resolver< - R, - Parent, - TContext - >; - export type IssuersResolver< - R = Maybe, - Parent = TlsNode, - TContext = SiemContext - > = Resolver; -} - export namespace UncommonProcessesDataResolvers { export interface Resolvers { edges?: EdgesResolver; @@ -9492,9 +9308,6 @@ export type IResolvers = { NetworkHttpItem?: NetworkHttpItemResolvers.Resolvers; OverviewNetworkData?: OverviewNetworkDataResolvers.Resolvers; OverviewHostData?: OverviewHostDataResolvers.Resolvers; - TlsData?: TlsDataResolvers.Resolvers; - TlsEdges?: TlsEdgesResolvers.Resolvers; - TlsNode?: TlsNodeResolvers.Resolvers; UncommonProcessesData?: UncommonProcessesDataResolvers.Resolvers; UncommonProcessesEdges?: UncommonProcessesEdgesResolvers.Resolvers; UncommonProcessItem?: UncommonProcessItemResolvers.Resolvers; diff --git a/x-pack/plugins/security_solution/server/init_server.ts b/x-pack/plugins/security_solution/server/init_server.ts index 1463d7f0da284..2ef42eaee4b98 100644 --- a/x-pack/plugins/security_solution/server/init_server.ts +++ b/x-pack/plugins/security_solution/server/init_server.ts @@ -28,7 +28,6 @@ import { createTimelineResolvers } from './graphql/timeline'; import { createUncommonProcessesResolvers } from './graphql/uncommon_processes'; import { createWhoAmIResolvers } from './graphql/who_am_i'; import { AppBackendLibs } from './lib/types'; -import { createTlsResolvers } from './graphql/tls'; import { createMatrixHistogramResolvers } from './graphql/matrix_histogram'; export const initServer = (libs: AppBackendLibs) => { @@ -55,7 +54,6 @@ export const initServer = (libs: AppBackendLibs) => { createSourcesResolvers(libs) as IResolvers, createSourceStatusResolvers(libs) as IResolvers, createTimelineResolvers(libs) as IResolvers, - createTlsResolvers(libs) as IResolvers, createUncommonProcessesResolvers(libs) as IResolvers, createWhoAmIResolvers() as IResolvers, createKpiHostsResolvers(libs) as IResolvers, diff --git a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts index db76f6d52dbb0..bab00e33e3378 100644 --- a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts +++ b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts @@ -17,7 +17,6 @@ import { ElasticsearchKpiHostsAdapter } from '../kpi_hosts/elasticsearch_adapter import { ElasticsearchIndexFieldAdapter, IndexFields } from '../index_fields'; import { ElasticsearchIpDetailsAdapter, IpDetails } from '../ip_details'; -import { ElasticsearchTlsAdapter, TLS } from '../tls'; import { KpiNetwork } from '../kpi_network'; import { ElasticsearchKpiNetworkAdapter } from '../kpi_network/elasticsearch_adapter'; @@ -50,7 +49,6 @@ export function compose( fields: new IndexFields(new ElasticsearchIndexFieldAdapter(framework)), hosts: new Hosts(new ElasticsearchHostsAdapter(framework, endpointContext)), ipDetails: new IpDetails(new ElasticsearchIpDetailsAdapter(framework)), - tls: new TLS(new ElasticsearchTlsAdapter(framework)), kpiHosts: new KpiHosts(new ElasticsearchKpiHostsAdapter(framework)), kpiNetwork: new KpiNetwork(new ElasticsearchKpiNetworkAdapter(framework)), matrixHistogram: new MatrixHistogram(new ElasticsearchMatrixHistogramAdapter(framework)), diff --git a/x-pack/plugins/security_solution/server/lib/tls/elasticsearch_adapter.test.ts b/x-pack/plugins/security_solution/server/lib/tls/elasticsearch_adapter.test.ts deleted file mode 100644 index 428685cbaddb8..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/tls/elasticsearch_adapter.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { buildTlsQuery } from './query_tls.dsl'; -import { ElasticsearchTlsAdapter } from './elasticsearch_adapter'; -import expect from '@kbn/expect'; -import { FrameworkRequest, FrameworkAdapter } from '../framework'; -import { mockRequest, mockResponse, mockOptions, expectedTlsEdges, mockTlsQuery } from './mock'; -import { TlsData } from '../../graphql/types'; - -jest.mock('./query_tls.dsl', () => { - return { - buildTlsQuery: jest.fn(), - }; -}); - -describe('elasticsearch_adapter', () => { - describe('#getTls', () => { - let data: TlsData; - const mockCallWithRequest = jest.fn(); - const mockFramework: FrameworkAdapter = { - callWithRequest: mockCallWithRequest, - registerGraphQLEndpoint: jest.fn(), - getIndexPatternsService: jest.fn(), - }; - - beforeAll(async () => { - (buildTlsQuery as jest.Mock).mockReset(); - (buildTlsQuery as jest.Mock).mockReturnValue(mockTlsQuery); - - mockCallWithRequest.mockResolvedValue(mockResponse); - jest.doMock('../framework', () => ({ - callWithRequest: mockCallWithRequest, - })); - - const EsTls = new ElasticsearchTlsAdapter(mockFramework); - data = await EsTls.getTls(mockRequest as FrameworkRequest, mockOptions); - }); - - afterAll(() => { - mockCallWithRequest.mockRestore(); - (buildTlsQuery as jest.Mock).mockClear(); - }); - - test('buildTlsQuery', () => { - expect((buildTlsQuery as jest.Mock).mock.calls[0][0]).to.eql(mockOptions); - }); - - test('will return tlsEdges correctly', () => { - expect(data.edges).to.eql(expectedTlsEdges); - }); - - test('will return inspect data', () => { - expect(data.inspect).to.eql({ - dsl: [JSON.stringify(mockTlsQuery, null, 2)], - response: [JSON.stringify(mockResponse, null, 2)], - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/tls/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/tls/elasticsearch_adapter.ts deleted file mode 100644 index ab9175951a8f5..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/tls/elasticsearch_adapter.ts +++ /dev/null @@ -1,82 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; - -import { TlsData, TlsEdges } from '../../graphql/types'; -import { inspectStringifyObject } from '../../utils/build_query'; -import { DatabaseSearchResponse, FrameworkAdapter, FrameworkRequest } from '../framework'; -import { TermAggregation } from '../types'; -import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; -import { TlsRequestOptions } from './index'; - -import { TlsAdapter, TlsBuckets } from './types'; - -import { buildTlsQuery } from './query_tls.dsl'; - -export class ElasticsearchTlsAdapter implements TlsAdapter { - constructor(private readonly framework: FrameworkAdapter) {} - - public async getTls(request: FrameworkRequest, options: TlsRequestOptions): Promise { - if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { - throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); - } - const dsl = buildTlsQuery(options); - const response = await this.framework.callWithRequest( - request, - 'search', - dsl - ); - - const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; - const totalCount = getOr(0, 'aggregations.count.value', response); - const tlsEdges: TlsEdges[] = getTlsEdges(response, options); - const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; - const edges = tlsEdges.splice(cursorStart, querySize - cursorStart); - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - const showMorePagesIndicator = totalCount > fakeTotalCount; - return { - edges, - inspect, - pageInfo: { - activePage: activePage ? activePage : 0, - fakeTotalCount, - showMorePagesIndicator, - }, - totalCount, - }; - } -} - -const getTlsEdges = ( - response: DatabaseSearchResponse, - options: TlsRequestOptions -): TlsEdges[] => { - return formatTlsEdges(getOr([], 'aggregations.sha1.buckets', response)); -}; - -export const formatTlsEdges = (buckets: TlsBuckets[]): TlsEdges[] => { - return buckets.map((bucket: TlsBuckets) => { - const edge: TlsEdges = { - node: { - _id: bucket.key, - subjects: bucket.subjects.buckets.map(({ key }) => key), - ja3: bucket.ja3.buckets.map(({ key }) => key), - issuers: bucket.issuers.buckets.map(({ key }) => key), - // eslint-disable-next-line @typescript-eslint/naming-convention - notAfter: bucket.not_after.buckets.map(({ key_as_string }) => key_as_string), - }, - cursor: { - value: bucket.key, - tiebreaker: null, - }, - }; - return edge; - }); -}; diff --git a/x-pack/plugins/security_solution/server/lib/tls/index.ts b/x-pack/plugins/security_solution/server/lib/tls/index.ts deleted file mode 100644 index 25e3957cc99db..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/tls/index.ts +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FlowTargetSourceDest, TlsSortField, TlsData } from '../../graphql/types'; -import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; - -import { TlsAdapter } from './types'; - -export * from './elasticsearch_adapter'; - -export interface TlsRequestOptions extends RequestOptionsPaginated { - ip?: string; - sort: TlsSortField; - flowTarget: FlowTargetSourceDest; -} - -export class TLS { - constructor(private readonly adapter: TlsAdapter) {} - - public async getTls(req: FrameworkRequest, options: TlsRequestOptions): Promise { - return this.adapter.getTls(req, options); - } -} diff --git a/x-pack/plugins/security_solution/server/lib/tls/mock.ts b/x-pack/plugins/security_solution/server/lib/tls/mock.ts deleted file mode 100644 index 62d5e1e61570a..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/tls/mock.ts +++ /dev/null @@ -1,481 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Direction, TlsFields, FlowTargetSourceDest } from '../../graphql/types'; - -export const mockTlsQuery = { - allowNoIndices: true, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - ignoreUnavailable: true, - body: { - aggs: { - count: { cardinality: { field: 'tls.server_certificate.fingerprint.sha1' } }, - sha1: { - terms: { - field: 'tls.server_certificate.fingerprint.sha1', - size: 10, - order: { _key: 'desc' }, - }, - aggs: { - issuers: { terms: { field: 'tls.server.issuer' } }, - subjects: { terms: { field: 'tls.server.subject' } }, - not_after: { terms: { field: 'tls.server.not_after' } }, - ja3: { terms: { field: 'tls.server.ja3s' } }, - }, - }, - }, - query: { - bool: { filter: [{ range: { '@timestamp': { gte: 1570719927430, lte: 1570806327431 } } }] }, - }, - size: 0, - track_total_hits: false, - }, -}; - -export const expectedTlsEdges = [ - { - cursor: { - tiebreaker: null, - value: 'fff8dc95436e0e25ce46b1526a1a547e8cf3bb82', - }, - node: { - _id: 'fff8dc95436e0e25ce46b1526a1a547e8cf3bb82', - subjects: ['*.1.nflxso.net'], - issuers: ['DigiCert SHA2 Secure Server CA'], - ja3: ['95d2dd53a89b334cddd5c22e81e7fe61'], - notAfter: ['2019-10-27T12:00:00.000Z'], - }, - }, - { - cursor: { - tiebreaker: null, - value: 'fd8440c4b20978b173e0910e2639d114f0d405c5', - }, - node: { - _id: 'fd8440c4b20978b173e0910e2639d114f0d405c5', - subjects: ['cogocast.net'], - issuers: ['Amazon'], - ja3: ['a111d93cdf31f993c40a8a9ef13e8d7e'], - notAfter: ['2020-02-01T12:00:00.000Z'], - }, - }, - { - cursor: { tiebreaker: null, value: 'fcdc16645ebb3386adc96e7ba735c4745709b9dd' }, - node: { - _id: 'fcdc16645ebb3386adc96e7ba735c4745709b9dd', - subjects: ['player-devintever2.mountain.siriusxm.com'], - issuers: ['Trustwave Organization Validation SHA256 CA, Level 1'], - ja3: ['6fa3244afc6bb6f9fad207b6b52af26b'], - notAfter: ['2020-03-06T21:57:09.000Z'], - }, - }, - { - cursor: { tiebreaker: null, value: 'fccf375789cb7e671502a7b0cc969f218a4b2c70' }, - node: { - _id: 'fccf375789cb7e671502a7b0cc969f218a4b2c70', - subjects: ['appleid.apple.com'], - issuers: ['DigiCert SHA2 Extended Validation Server CA'], - ja3: ['6fa3244afc6bb6f9fad207b6b52af26b'], - notAfter: ['2020-07-04T12:00:00.000Z'], - }, - }, - { - cursor: { tiebreaker: null, value: 'fc4a296b706fa18ac50b96f5c0327c69db4a8981' }, - node: { - _id: 'fc4a296b706fa18ac50b96f5c0327c69db4a8981', - subjects: ['itunes.apple.com'], - issuers: ['DigiCert SHA2 Extended Validation Server CA'], - ja3: ['a441a33aaee795f498d6b764cc78989a'], - notAfter: ['2020-03-24T12:00:00.000Z'], - }, - }, - { - cursor: { tiebreaker: null, value: 'fc2cbc41f6a0e9c0118de4fe40f299f7207b797e' }, - node: { - _id: 'fc2cbc41f6a0e9c0118de4fe40f299f7207b797e', - subjects: ['incapsula.com'], - issuers: ['GlobalSign CloudSSL CA - SHA256 - G3'], - ja3: ['6fa3244afc6bb6f9fad207b6b52af26b'], - notAfter: ['2020-04-04T14:05:06.000Z'], - }, - }, - { - cursor: { tiebreaker: null, value: 'fb70d78ffa663a3a4374d841b3288d2de9759566' }, - node: { - _id: 'fb70d78ffa663a3a4374d841b3288d2de9759566', - subjects: ['*.siriusxm.com'], - issuers: ['DigiCert Baltimore CA-2 G2'], - ja3: ['535aca3d99fc247509cd50933cd71d37', '6fa3244afc6bb6f9fad207b6b52af26b'], - notAfter: ['2021-10-27T12:00:00.000Z'], - }, - }, - { - cursor: { tiebreaker: null, value: 'fb59038dcec33ab3a01a6ae60d0835ad0e04ccf0' }, - node: { - _id: 'fb59038dcec33ab3a01a6ae60d0835ad0e04ccf0', - subjects: ['photos.amazon.eu'], - issuers: ['Amazon'], - ja3: ['6fa3244afc6bb6f9fad207b6b52af26b'], - notAfter: ['2020-04-23T12:00:00.000Z'], - }, - }, - { - cursor: { tiebreaker: null, value: 'f9815293c883a6006f0b2d95a4895bdc501fd174' }, - node: { - _id: 'f9815293c883a6006f0b2d95a4895bdc501fd174', - subjects: ['cdn.hbo.com'], - issuers: ['Sectigo RSA Organization Validation Secure Server CA'], - ja3: ['6fa3244afc6bb6f9fad207b6b52af26b'], - notAfter: ['2021-02-10T23:59:59.000Z'], - }, - }, - { - cursor: { tiebreaker: null, value: 'f8db6a69797e383dca2529727369595733123386' }, - node: { - _id: 'f8db6a69797e383dca2529727369595733123386', - subjects: ['www.google.com'], - issuers: ['GTS CA 1O1'], - ja3: ['a111d93cdf31f993c40a8a9ef13e8d7e'], - notAfter: ['2019-12-10T13:32:54.000Z'], - }, - }, -]; - -export const mockRequest = { - body: { - operationName: 'GetTlsQuery', - variables: { - defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - filterQuery: '', - flowTarget: 'source', - inspect: false, - ip: '', - pagination: { activePage: 0, cursorStart: 0, fakePossibleCount: 50, querySize: 10 }, - sort: { field: '_id', direction: 'desc' }, - sourceId: 'default', - timerange: { interval: '12h', from: 1570716261267, to: 1570802661267 }, - }, - query: - 'query GetTlsQuery($sourceId: ID!, $filterQuery: String, $flowTarget: FlowTarget!, $ip: String!, $pagination: PaginationInputPaginated!, $sort: TlsSortField!, $timerange: TimerangeInput!, $defaultIndex: [String!]!, $inspect: Boolean!) {\n source(id: $sourceId) {\n id\n Tls(filterQuery: $filterQuery, flowTarget: $flowTarget, ip: $ip, pagination: $pagination, sort: $sort, timerange: $timerange, defaultIndex: $defaultIndex) {\n totalCount\n edges {\n node {\n _id\n subjects\n ja3\n issuers\n notAfter\n __typename\n }\n cursor {\n value\n __typename\n }\n __typename\n }\n pageInfo {\n activePage\n fakeTotalCount\n showMorePagesIndicator\n __typename\n }\n inspect @include(if: $inspect) {\n dsl\n response\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', - }, -}; - -export const mockResponse = { - took: 92, - timed_out: false, - _shards: { total: 33, successful: 33, skipped: 0, failed: 0 }, - hits: { max_score: null, hits: [] }, - aggregations: { - sha1: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 4597, - buckets: [ - { - key: 'fff8dc95436e0e25ce46b1526a1a547e8cf3bb82', - doc_count: 1, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1572177600000, key_as_string: '2019-10-27T12:00:00.000Z', doc_count: 1 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'DigiCert SHA2 Secure Server CA', doc_count: 1 }], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: '*.1.nflxso.net', doc_count: 1 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: '95d2dd53a89b334cddd5c22e81e7fe61', doc_count: 1 }], - }, - }, - { - key: 'fd8440c4b20978b173e0910e2639d114f0d405c5', - doc_count: 1, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1580558400000, key_as_string: '2020-02-01T12:00:00.000Z', doc_count: 1 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'Amazon', doc_count: 1 }], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'cogocast.net', doc_count: 1 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'a111d93cdf31f993c40a8a9ef13e8d7e', doc_count: 1 }], - }, - }, - { - key: 'fcdc16645ebb3386adc96e7ba735c4745709b9dd', - doc_count: 1, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1583531829000, key_as_string: '2020-03-06T21:57:09.000Z', doc_count: 1 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'Trustwave Organization Validation SHA256 CA, Level 1', doc_count: 1 }, - ], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'player-devintever2.mountain.siriusxm.com', doc_count: 1 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: '6fa3244afc6bb6f9fad207b6b52af26b', doc_count: 1 }], - }, - }, - { - key: 'fccf375789cb7e671502a7b0cc969f218a4b2c70', - doc_count: 1, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1593864000000, key_as_string: '2020-07-04T12:00:00.000Z', doc_count: 1 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'DigiCert SHA2 Extended Validation Server CA', doc_count: 1 }], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'appleid.apple.com', doc_count: 1 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: '6fa3244afc6bb6f9fad207b6b52af26b', doc_count: 1 }], - }, - }, - { - key: 'fc4a296b706fa18ac50b96f5c0327c69db4a8981', - doc_count: 2, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1585051200000, key_as_string: '2020-03-24T12:00:00.000Z', doc_count: 2 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'DigiCert SHA2 Extended Validation Server CA', doc_count: 2 }], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'itunes.apple.com', doc_count: 2 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'a441a33aaee795f498d6b764cc78989a', doc_count: 2 }], - }, - }, - { - key: 'fc2cbc41f6a0e9c0118de4fe40f299f7207b797e', - doc_count: 1, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1586009106000, key_as_string: '2020-04-04T14:05:06.000Z', doc_count: 1 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'GlobalSign CloudSSL CA - SHA256 - G3', doc_count: 1 }], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'incapsula.com', doc_count: 1 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: '6fa3244afc6bb6f9fad207b6b52af26b', doc_count: 1 }], - }, - }, - { - key: 'fb70d78ffa663a3a4374d841b3288d2de9759566', - doc_count: 325, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1635336000000, key_as_string: '2021-10-27T12:00:00.000Z', doc_count: 325 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'DigiCert Baltimore CA-2 G2', doc_count: 325 }], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: '*.siriusxm.com', doc_count: 325 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: '535aca3d99fc247509cd50933cd71d37', doc_count: 284 }, - { key: '6fa3244afc6bb6f9fad207b6b52af26b', doc_count: 39 }, - ], - }, - }, - { - key: 'fb59038dcec33ab3a01a6ae60d0835ad0e04ccf0', - doc_count: 5, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1587643200000, key_as_string: '2020-04-23T12:00:00.000Z', doc_count: 5 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'Amazon', doc_count: 5 }], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'photos.amazon.eu', doc_count: 5 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: '6fa3244afc6bb6f9fad207b6b52af26b', doc_count: 5 }], - }, - }, - { - key: 'f9815293c883a6006f0b2d95a4895bdc501fd174', - doc_count: 29, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1613001599000, key_as_string: '2021-02-10T23:59:59.000Z', doc_count: 29 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'Sectigo RSA Organization Validation Secure Server CA', doc_count: 29 }, - ], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'cdn.hbo.com', doc_count: 29 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: '6fa3244afc6bb6f9fad207b6b52af26b', doc_count: 26 }], - }, - }, - { - key: 'f8db6a69797e383dca2529727369595733123386', - doc_count: 5, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1575984774000, key_as_string: '2019-12-10T13:32:54.000Z', doc_count: 5 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'GTS CA 1O1', doc_count: 5 }], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'www.google.com', doc_count: 5 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'a111d93cdf31f993c40a8a9ef13e8d7e', doc_count: 5 }], - }, - }, - ], - }, - count: { value: 364 }, - }, -}; - -export const mockOptions = { - defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - sourceConfiguration: { - fields: { - container: 'docker.container.name', - host: 'beat.hostname', - message: ['message', '@message'], - pod: 'kubernetes.pod.name', - tiebreaker: '_doc', - timestamp: '@timestamp', - }, - }, - timerange: { interval: '12h', to: '2019-10-11T13:51:11.626Z', from: '2019-10-10T13:51:11.626Z' }, - pagination: { activePage: 0, cursorStart: 0, fakePossibleCount: 50, querySize: 10 }, - filterQuery: {}, - fields: [ - 'totalCount', - '_id', - 'subjects', - 'ja3', - 'issuers', - 'notAfter', - 'edges.cursor.value', - 'pageInfo.activePage', - 'pageInfo.fakeTotalCount', - 'pageInfo.showMorePagesIndicator', - 'inspect.dsl', - 'inspect.response', - ], - ip: '', - sort: { field: TlsFields._id, direction: Direction.desc }, - flowTarget: FlowTargetSourceDest.source, -}; diff --git a/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts b/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts deleted file mode 100644 index f6921ddcdf508..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts +++ /dev/null @@ -1,107 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { assertUnreachable } from '../../../common/utility_types'; -import { createQueryFilterClauses } from '../../utils/build_query'; - -import { TlsRequestOptions } from './index'; -import { TlsSortField, Direction, TlsFields } from '../../graphql/types'; - -const getAggs = (querySize: number, sort: TlsSortField) => ({ - count: { - cardinality: { - field: 'tls.server.hash.sha1', - }, - }, - sha1: { - terms: { - field: 'tls.server.hash.sha1', - size: querySize, - order: { - ...getQueryOrder(sort), - }, - }, - aggs: { - issuers: { - terms: { - field: 'tls.server.issuer', - }, - }, - subjects: { - terms: { - field: 'tls.server.subject', - }, - }, - not_after: { - terms: { - field: 'tls.server.not_after', - }, - }, - ja3: { - terms: { - field: 'tls.server.ja3s', - }, - }, - }, - }, -}); - -export const buildTlsQuery = ({ - ip, - sort, - filterQuery, - flowTarget, - pagination: { querySize }, - defaultIndex, - sourceConfiguration: { - fields: { timestamp }, - }, - timerange: { from, to }, -}: TlsRequestOptions) => { - const defaultFilter = [ - ...createQueryFilterClauses(filterQuery), - { - range: { - [timestamp]: { gte: from, lte: to, format: 'strict_date_optional_time' }, - }, - }, - ]; - - const filter = ip ? [...defaultFilter, { term: { [`${flowTarget}.ip`]: ip } }] : defaultFilter; - - const dslQuery = { - allowNoIndices: true, - index: defaultIndex, - ignoreUnavailable: true, - body: { - aggs: { - ...getAggs(querySize, sort), - }, - query: { - bool: { - filter, - }, - }, - size: 0, - track_total_hits: false, - }, - }; - - return dslQuery; -}; - -interface QueryOrder { - _key: Direction; -} - -const getQueryOrder = (sort: TlsSortField): QueryOrder => { - switch (sort.field) { - case TlsFields._id: - return { _key: sort.direction }; - default: - return assertUnreachable(sort.field); - } -}; diff --git a/x-pack/plugins/security_solution/server/lib/tls/types.ts b/x-pack/plugins/security_solution/server/lib/tls/types.ts deleted file mode 100644 index f18ddc04e14a0..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/tls/types.ts +++ /dev/null @@ -1,36 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FrameworkRequest, RequestBasicOptions } from '../framework'; -import { TlsData } from '../../graphql/types'; - -export interface TlsAdapter { - getTls(request: FrameworkRequest, options: RequestBasicOptions): Promise; -} - -export interface TlsBuckets { - key: string; - timestamp?: { - value: number; - value_as_string: string; - }; - - subjects: { - buckets: Readonly>; - }; - - ja3: { - buckets: Readonly>; - }; - - issuers: { - buckets: Readonly>; - }; - - not_after: { - buckets: Readonly>; - }; -} diff --git a/x-pack/plugins/security_solution/server/lib/types.ts b/x-pack/plugins/security_solution/server/lib/types.ts index 435bcd9d61d89..4f70e3aa8652a 100644 --- a/x-pack/plugins/security_solution/server/lib/types.ts +++ b/x-pack/plugins/security_solution/server/lib/types.ts @@ -24,7 +24,6 @@ import { UncommonProcesses } from './uncommon_processes'; import { Note } from './note/saved_object'; import { PinnedEvent } from './pinned_event/saved_object'; import { Timeline } from './timeline/saved_object'; -import { TLS } from './tls'; import { MatrixHistogram } from './matrix_histogram'; export * from './hosts'; @@ -41,7 +40,6 @@ export interface AppDomainLibs { overview: Overview; uncommonProcesses: UncommonProcesses; kpiHosts: KpiHosts; - tls: TLS; } export interface AppBackendLibs extends AppDomainLibs { diff --git a/x-pack/test/api_integration/apis/security_solution/index.js b/x-pack/test/api_integration/apis/security_solution/index.js index b97795f325271..e4204ae295653 100644 --- a/x-pack/test/api_integration/apis/security_solution/index.js +++ b/x-pack/test/api_integration/apis/security_solution/index.js @@ -22,7 +22,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./timeline_details')); loadTestFile(require.resolve('./uncommon_processes')); loadTestFile(require.resolve('./users')); - loadTestFile(require.resolve('./tls')); + // loadTestFile(require.resolve('./tls')); loadTestFile(require.resolve('./feature_controls')); }); } diff --git a/x-pack/test/api_integration/apis/security_solution/tls.ts b/x-pack/test/api_integration/apis/security_solution/tls.ts index e5f6233d50d59..ebaec7783427f 100644 --- a/x-pack/test/api_integration/apis/security_solution/tls.ts +++ b/x-pack/test/api_integration/apis/security_solution/tls.ts @@ -5,11 +5,14 @@ */ import expect from '@kbn/expect'; +// @ts-expect-error import { tlsQuery } from '../../../../plugins/security_solution/public/network/containers/tls/index.gql_query'; import { Direction, + // @ts-expect-error TlsFields, FlowTarget, + // @ts-expect-error GetTlsQuery, } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; From 8c9c4c442cc86206999e3a22fab188ee65fefa8a Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 23 Sep 2020 08:48:38 -0400 Subject: [PATCH 41/92] [Ingest pipelines] Implement empty prompt for processors editor (#77655) --- .../ingest_pipelines_create.test.tsx | 25 ------- .../pipeline_form/pipeline_form.tsx | 2 - .../pipeline_form/pipeline_form_fields.tsx | 26 +------ .../pipeline_processors_editor.helpers.tsx | 10 +-- .../pipeline_processors_editor.test.tsx | 17 +++++ .../components/add_processor_button.tsx | 37 +++++++--- .../components/index.ts | 6 ++ .../components/load_from_json/button.tsx | 2 +- .../on_failure_processors_title.tsx | 2 +- .../processor_form.container.tsx | 20 ++--- .../components/processors_empty_prompt.tsx | 73 +++++++++++++++++++ .../components}/processors_header.tsx | 33 +++++---- .../processors_tree/components/tree_node.tsx | 1 + .../processors_tree/processors_tree.tsx | 3 +- .../pipeline_processors_editor/index.ts | 2 + .../pipeline_processors_editor.scss} | 0 .../pipeline_processors_editor.tsx | 66 +++++++++++++++++ 17 files changed, 232 insertions(+), 93 deletions(-) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_form => pipeline_processors_editor/components}/on_failure_processors_title.tsx (96%) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_empty_prompt.tsx rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_form => pipeline_processors_editor/components}/processors_header.tsx (78%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_form/pipeline_form.scss => pipeline_processors_editor/pipeline_processors_editor.scss} (100%) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx index 6074c64d2bdb0..18ca71f2bb73a 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx @@ -185,30 +185,5 @@ describe('', () => { expect(find('savePipelineError').find('li').length).toBe(8); }); }); - - describe('test pipeline', () => { - beforeEach(async () => { - await act(async () => { - testBed = await setup(); - - const { waitFor } = testBed; - - await waitFor('pipelineForm'); - }); - }); - - test('should open the test pipeline flyout', async () => { - const { actions, exists, find, waitFor } = testBed; - - await act(async () => { - actions.clickAddDocumentsButton(); - await waitFor('testPipelineFlyout'); - }); - - // Verify test pipeline flyout opens - expect(exists('testPipelineFlyout')).toBe(true); - expect(find('testPipelineFlyout.title').text()).toBe('Test pipeline'); - }); - }); }); }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx index 5279bd718c16e..ffd82b0bbaf35 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -11,8 +11,6 @@ import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from import { useForm, Form, FormConfig } from '../../../shared_imports'; import { Pipeline, Processor } from '../../../../common/types'; -import './pipeline_form.scss'; - import { OnUpdateHandlerArg, OnUpdateHandler } from '../pipeline_processors_editor'; import { PipelineRequestFlyout } from './pipeline_request_flyout'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx index 6033f34af6825..a7ffe7ba02caa 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx @@ -6,7 +6,7 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiSwitch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiSpacer, EuiSwitch } from '@elastic/eui'; import { Processor } from '../../../../common/types'; @@ -14,15 +14,11 @@ import { getUseField, getFormRow, Field } from '../../../shared_imports'; import { ProcessorsEditorContextProvider, - GlobalOnFailureProcessorsEditor, - ProcessorsEditor, OnUpdateHandler, OnDoneLoadJsonHandler, + PipelineProcessorsEditor, } from '../pipeline_processors_editor'; -import { ProcessorsHeader } from './processors_header'; -import { OnFailureProcessorsTitle } from './on_failure_processors_title'; - interface Props { processors: Processor[]; onFailure?: Processor[]; @@ -118,28 +114,12 @@ export const PipelineFormFields: React.FunctionComponent = ({ {/* Pipeline Processors Editor */} - -
- - - - - - - - - - - - - - -
+
); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx index e46e5156e30f3..10fb73df1ce1c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx @@ -11,12 +11,7 @@ import { notificationServiceMock, scopedHistoryMock } from 'src/core/public/mock import { LocationDescriptorObject } from 'history'; import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; import { registerTestBed, TestBed } from '../../../../../../../test_utils'; -import { - ProcessorsEditorContextProvider, - Props, - ProcessorsEditor, - GlobalOnFailureProcessorsEditor, -} from '../'; +import { ProcessorsEditorContextProvider, Props, PipelineProcessorsEditor } from '../'; import { breadcrumbService, @@ -90,7 +85,7 @@ const testBedSetup = registerTestBed( (props: Props) => ( - + ), @@ -210,4 +205,5 @@ type TestSubject = | 'processorSettingsFormFlyout' | 'processorTypeSelector' | 'pipelineEditorOnFailureTree' + | 'processorsEmptyPrompt' | string; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx index 74ae8b8894b9f..b80d238362118 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx @@ -55,6 +55,23 @@ describe('Pipeline Editor', () => { expect(arg.getData()).toEqual(testProcessors); }); + describe('no processors', () => { + beforeEach(async () => { + testBed = await setup({ + value: { + processors: [], + }, + onFlyoutOpen: jest.fn(), + onUpdate, + }); + }); + + it('displays an empty prompt if no processors are defined', () => { + const { exists } = testBed; + expect(exists('processorsEmptyPrompt')).toBe(true); + }); + }); + describe('processors', () => { it('adds a new processor', async () => { const { actions } = testBed; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx index 4aabcc1d59d73..03b497320dfbc 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx @@ -6,30 +6,49 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonEmpty } from '@elastic/eui'; +import { EuiButtonEmpty, EuiButton } from '@elastic/eui'; import { usePipelineProcessorsContext } from '../context'; export interface Props { onClick: () => void; + renderButtonAsLink?: boolean; } +const addProcessorButtonLabel = i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.addProcessorButtonLabel', + { + defaultMessage: 'Add a processor', + } +); + export const AddProcessorButton: FunctionComponent = (props) => { - const { onClick } = props; + const { onClick, renderButtonAsLink } = props; const { state: { editor }, } = usePipelineProcessorsContext(); + + if (renderButtonAsLink) { + return ( + + {addProcessorButtonLabel} + + ); + } + return ( - - {i18n.translate('xpack.ingestPipelines.pipelineEditor.addProcessorButtonLabel', { - defaultMessage: 'Add a processor', - })} - + {addProcessorButtonLabel} +
); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts index d476202aa43bb..2e62a81ffa153 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts @@ -19,3 +19,9 @@ export { OnDoneLoadJsonHandler, LoadFromJsonButton } from './load_from_json'; export { TestPipelineActions } from './test_pipeline'; export { PipelineProcessorsItemTooltip, Position } from './pipeline_processors_editor_item_tooltip'; + +export { ProcessorsEmptyPrompt } from './processors_empty_prompt'; + +export { ProcessorsHeader } from './processors_header'; + +export { OnFailureProcessorsTitle } from './on_failure_processors_title'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx index 21d15fc86a0ce..38700d6a7a87c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx @@ -15,7 +15,7 @@ interface Props { const i18nTexts = { buttonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.buttonLabel', { - defaultMessage: 'Import', + defaultMessage: 'Import processors', }), }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/on_failure_processors_title.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx similarity index 96% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/on_failure_processors_title.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx index 0beb5657b54cb..7adc37d1897d1 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/on_failure_processors_title.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx @@ -8,7 +8,7 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibana } from '../../../shared_imports'; +import { useKibana } from '../../../../shared_imports'; export const OnFailureProcessorsTitle: FunctionComponent = () => { const { services } = useKibana(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_form.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_form.container.tsx index 332908d0756f2..c3b1799ac2a28 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_form.container.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_form.container.tsx @@ -113,15 +113,15 @@ export const ProcessorFormContainer: FunctionComponent = ({ handleSubmit={handleSubmit} /> ); - } else { - return ( - - ); } + + return ( + + ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_empty_prompt.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_empty_prompt.tsx new file mode 100644 index 0000000000000..3750ddda25d10 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_empty_prompt.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiEmptyPrompt, EuiSpacer, EuiLink } from '@elastic/eui'; +import { useKibana } from '../../../../shared_imports'; +import { usePipelineProcessorsContext } from '../context'; +import { AddProcessorButton } from './add_processor_button'; +import { OnDoneLoadJsonHandler, LoadFromJsonButton } from './load_from_json'; + +const i18nTexts = { + emptyPromptTitle: i18n.translate('xpack.ingestPipelines.pipelineEditor.emptyPrompt.title', { + defaultMessage: 'Add your first processor', + }), +}; + +export interface Props { + onLoadJson: OnDoneLoadJsonHandler; +} + +export const ProcessorsEmptyPrompt: FunctionComponent = ({ onLoadJson }) => { + const { onTreeAction } = usePipelineProcessorsContext(); + const { services } = useKibana(); + + return ( + {i18nTexts.emptyPromptTitle}} + data-test-subj="processorsEmptyPrompt" + body={ +

+ + {i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.processorsDocumentationLink', + { + defaultMessage: 'Learn more.', + } + )} + + ), + }} + /> +

+ } + actions={ + <> + { + onTreeAction({ type: 'addProcessor', payload: { target: ['processors'] } }); + }} + /> + + + + + + } + /> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_header.tsx similarity index 78% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_header.tsx index 43477affa8d94..24f3207d6bea4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_header.tsx @@ -9,21 +9,32 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiTitle } from '@elastic/ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibana } from '../../../shared_imports'; +import { useKibana } from '../../../../shared_imports'; -import { - LoadFromJsonButton, - OnDoneLoadJsonHandler, - TestPipelineActions, -} from '../pipeline_processors_editor'; +import { LoadFromJsonButton, OnDoneLoadJsonHandler, TestPipelineActions } from './'; export interface Props { onLoadJson: OnDoneLoadJsonHandler; + hasProcessors: boolean; } -export const ProcessorsHeader: FunctionComponent = ({ onLoadJson }) => { +export const ProcessorsHeader: FunctionComponent = ({ onLoadJson, hasProcessors }) => { const { services } = useKibana(); + const ProcessorTitle: FunctionComponent = () => ( + +

+ {i18n.translate('xpack.ingestPipelines.pipelineEditor.processorsTreeTitle', { + defaultMessage: 'Processors', + })} +

+
+ ); + + if (!hasProcessors) { + return ; + } + return ( = ({ onLoadJson }) => { - -

- {i18n.translate('xpack.ingestPipelines.pipelineEditor.processorsTreeTitle', { - defaultMessage: 'Processors', - })} -

-
+
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx index e9008e6f5b693..3a8299c017d8d 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx @@ -70,6 +70,7 @@ export const TreeNode: FunctionComponent = ({ /> onAction({ type: 'addProcessor', diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx index 8b344a137f3a8..ffc0a1459b791 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx @@ -99,7 +99,7 @@ export const ProcessorsTree: FunctionComponent = memo((props) => { - + {!processors.length && ( = memo((props) => { onClick={() => { onAction({ type: 'addProcessor', payload: { target: baseSelector } }); }} + renderButtonAsLink /> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts index c462b19c79327..ca5184da25a07 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts @@ -15,3 +15,5 @@ export { OnUpdateHandlerArg, OnUpdateHandler } from './types'; export { SerializeResult } from './serialize'; export { LoadFromJsonButton, OnDoneLoadJsonHandler, TestPipelineActions } from './components'; + +export { PipelineProcessorsEditor } from './pipeline_processors_editor'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.scss rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx new file mode 100644 index 0000000000000..beb165973d3cd --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { usePipelineProcessorsContext } from './context'; +import { + ProcessorsEmptyPrompt, + OnFailureProcessorsTitle, + ProcessorsHeader, + OnDoneLoadJsonHandler, +} from './components'; +import { ProcessorsEditor, GlobalOnFailureProcessorsEditor } from './editors'; + +import './pipeline_processors_editor.scss'; + +interface Props { + onLoadJson: OnDoneLoadJsonHandler; +} + +export const PipelineProcessorsEditor: React.FunctionComponent = ({ onLoadJson }) => { + const { + state: { processors: allProcessors }, + } = usePipelineProcessorsContext(); + + const { + state: { processors, onFailure }, + } = allProcessors; + + const showEmptyPrompt = processors.length === 0 && onFailure.length === 0; + + let content: React.ReactNode; + + if (showEmptyPrompt) { + content = ; + } else { + content = ( + <> + + + + + + + + + ); + } + + return ( +
+ + + 0} /> + + + {content} + + +
+ ); +}; From 21490d91c63b0bfc63ff1ff94f2235d29f7f41aa Mon Sep 17 00:00:00 2001 From: Bohdan Tsymbala Date: Wed, 23 Sep 2020 15:07:09 +0200 Subject: [PATCH 42/92] Btsymbala/trusted app deletion (#77316) * Moved the DeleteTrustedAppsRequestParams to common folder to be able to use it on the client. * Added trusted app deletion API to the client service layer. * Made default data type for async resource state. * Added guard for stale state. * Added timestamp to the list data to be used to refresh list when it's modified. * Separated out base type for resource state change actions. * Added action for outdating list data. * Moved the refresh condition inside the middleware case function and added timestamping data. * Added state, actions, reducers and middleware for deletion dialog. * Added actions column and deletion action. * Added trusted app deletion dialog. * Changed to not have deletonDialog as optional in store. * Changed the store to contain the full entry in the dialog state and changed the modal message to indicate the trusted app name. * Extracted notifications component and enhanced error display. * Added success message and unified messages a bit. * Complete coverage with tests. * Removed unused variable in translations. * Fixed tests because of outdated snapshots and inproper mocking of htmlIdGenerator. * Fixed code review comments. * Fixed type error. --- .../common/endpoint/types/trusted_apps.ts | 4 + .../public/management/common/routing.ts | 5 +- .../pages/trusted_apps/service/index.ts | 11 + .../pages/trusted_apps/service/utils.test.ts | 44 + .../pages/trusted_apps/service/utils.ts | 10 + .../state/async_resource_state.test.ts | 19 + .../state/async_resource_state.ts | 17 +- .../state/trusted_apps_list_page_state.ts | 9 +- .../pages/trusted_apps/store/action.ts | 34 +- .../trusted_apps/store/middleware.test.ts | 222 +- .../pages/trusted_apps/store/middleware.ts | 133 +- .../pages/trusted_apps/store/reducer.test.ts | 153 +- .../pages/trusted_apps/store/reducer.ts | 85 +- .../trusted_apps/store/selectors.test.ts | 375 ++- .../pages/trusted_apps/store/selectors.ts | 44 +- .../pages/trusted_apps/test_utils/index.ts | 73 +- .../trusted_app_deletion_dialog.test.tsx.snap | 315 +++ .../trusted_apps_list.test.tsx.snap | 2056 ++++++++++++++++- .../trusted_apps_page.test.tsx.snap | 36 +- .../view/trusted_app_deletion_dialog.test.tsx | 126 + .../view/trusted_app_deletion_dialog.tsx | 115 + .../view/trusted_apps_list.test.tsx | 66 +- .../trusted_apps/view/trusted_apps_list.tsx | 50 +- .../view/trusted_apps_notifications.test.tsx | 98 + .../view/trusted_apps_notifications.tsx | 58 + .../view/trusted_apps_page.test.tsx | 9 + .../trusted_apps/view/trusted_apps_page.tsx | 4 + .../public/management/store/reducer.ts | 2 +- .../endpoint/routes/trusted_apps/handlers.ts | 2 +- .../routes/trusted_apps/trusted_apps.test.ts | 2 +- .../endpoint/routes/trusted_apps/types.ts | 10 - 31 files changed, 3848 insertions(+), 339 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/utils.test.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/utils.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_app_deletion_dialog.test.tsx.snap create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx delete mode 100644 x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/types.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index 93e3305078f8d..62793388e34a6 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -6,10 +6,14 @@ import { TypeOf } from '@kbn/config-schema'; import { + DeleteTrustedAppsRequestSchema, GetTrustedAppsRequestSchema, PostTrustedAppCreateRequestSchema, } from '../schema/trusted_apps'; +/** API request params for deleting Trusted App entry */ +export type DeleteTrustedAppsRequestParams = TypeOf; + /** API request params for retrieving a list of Trusted Apps */ export type GetTrustedAppsListRequest = TypeOf; diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index 40320ed794203..cb4ed9b098fce 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -90,10 +90,7 @@ export const getPolicyDetailPath = (policyId: string, search?: string) => { })}${appendSearch(search)}`; }; -const isDefaultOrMissing = ( - value: number | string | undefined, - defaultValue: number | undefined -) => { +const isDefaultOrMissing = (value: T | undefined, defaultValue: T) => { return value === undefined || value === defaultValue; }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts index a3c5911aa3a86..4fb1e1b4575c8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts @@ -5,19 +5,26 @@ */ import { HttpStart } from 'kibana/public'; + import { TRUSTED_APPS_CREATE_API, + TRUSTED_APPS_DELETE_API, TRUSTED_APPS_LIST_API, } from '../../../../../common/endpoint/constants'; + import { + DeleteTrustedAppsRequestParams, GetTrustedListAppsResponse, GetTrustedAppsListRequest, PostTrustedAppCreateRequest, PostTrustedAppCreateResponse, } from '../../../../../common/endpoint/types/trusted_apps'; +import { resolvePathVariables } from './utils'; + export interface TrustedAppsService { getTrustedAppsList(request: GetTrustedAppsListRequest): Promise; + deleteTrustedApp(request: DeleteTrustedAppsRequestParams): Promise; createTrustedApp(request: PostTrustedAppCreateRequest): Promise; } @@ -30,6 +37,10 @@ export class TrustedAppsHttpService implements TrustedAppsService { }); } + async deleteTrustedApp(request: DeleteTrustedAppsRequestParams): Promise { + return this.http.delete(resolvePathVariables(TRUSTED_APPS_DELETE_API, request)); + } + async createTrustedApp(request: PostTrustedAppCreateRequest) { return this.http.post(TRUSTED_APPS_CREATE_API, { body: JSON.stringify(request), diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/utils.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/utils.test.ts new file mode 100644 index 0000000000000..c937b318e8961 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/utils.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resolvePathVariables } from './utils'; + +describe('utils', () => { + describe('resolvePathVariables', () => { + it('should resolve defined variables', () => { + expect(resolvePathVariables('/segment1/{var1}/segment2', { var1: 'value1' })).toBe( + '/segment1/value1/segment2' + ); + }); + + it('should not resolve undefined variables', () => { + expect(resolvePathVariables('/segment1/{var1}/segment2', {})).toBe( + '/segment1/{var1}/segment2' + ); + }); + + it('should ignore unused variables', () => { + expect(resolvePathVariables('/segment1/{var1}/segment2', { var2: 'value2' })).toBe( + '/segment1/{var1}/segment2' + ); + }); + + it('should replace multiple variable occurences', () => { + expect(resolvePathVariables('/{var1}/segment1/{var1}', { var1: 'value1' })).toBe( + '/value1/segment1/value1' + ); + }); + + it('should replace multiple variables', () => { + const path = resolvePathVariables('/{var1}/segment1/{var2}', { + var1: 'value1', + var2: 'value2', + }); + + expect(path).toBe('/value1/segment1/value2'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/utils.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/utils.ts new file mode 100644 index 0000000000000..075d74da018b4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/utils.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export const resolvePathVariables = (path: string, variables: { [K: string]: string | number }) => + Object.keys(variables).reduce((acc, paramName) => { + return acc.replace(new RegExp(`\{${paramName}\}`, 'g'), String(variables[paramName])); + }, path); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.test.ts index 5e00d833981ed..534a4ec14861b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.test.ts @@ -13,6 +13,7 @@ import { isLoadingResourceState, isLoadedResourceState, isFailedResourceState, + isStaleResourceState, getLastLoadedResourceState, getCurrentResourceError, isOutdatedResourceState, @@ -137,6 +138,24 @@ describe('AsyncResourceState', () => { expect(isFailedResourceState(failedResourceStateInitially)).toBe(true); }); }); + + describe('isStaleResourceState()', () => { + it('returns true for UninitialisedResourceState', () => { + expect(isStaleResourceState(uninitialisedResourceState)).toBe(true); + }); + + it('returns false for LoadingResourceState', () => { + expect(isStaleResourceState(loadingResourceStateInitially)).toBe(false); + }); + + it('returns true for LoadedResourceState', () => { + expect(isStaleResourceState(loadedResourceState)).toBe(true); + }); + + it('returns true for FailedResourceState', () => { + expect(isStaleResourceState(failedResourceStateInitially)).toBe(true); + }); + }); }); describe('functions', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.ts index 4639a50a61865..bb868418e7f0d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.ts @@ -35,7 +35,7 @@ export interface UninitialisedResourceState { * @param Data - type of the data that is referenced by resource state * @param Error - type of the error that can happen during attempt to update data */ -export interface LoadingResourceState { +export interface LoadingResourceState { type: 'LoadingResourceState'; previousState: StaleResourceState; } @@ -46,7 +46,7 @@ export interface LoadingResourceState { * * @param Data - type of the data that is referenced by resource state */ -export interface LoadedResourceState { +export interface LoadedResourceState { type: 'LoadedResourceState'; data: Data; } @@ -59,7 +59,7 @@ export interface LoadedResourceState { * @param Data - type of the data that is referenced by resource state * @param Error - type of the error that can happen during attempt to update data */ -export interface FailedResourceState { +export interface FailedResourceState { type: 'FailedResourceState'; error: Error; lastLoadedState?: LoadedResourceState; @@ -71,7 +71,7 @@ export interface FailedResourceState { * @param Data - type of the data that is referenced by resource state * @param Error - type of the error that can happen during attempt to update data */ -export type StaleResourceState = +export type StaleResourceState = | UninitialisedResourceState | LoadedResourceState | FailedResourceState; @@ -82,7 +82,7 @@ export type StaleResourceState = * @param Data - type of the data that is referenced by resource state * @param Error - type of the error that can happen during attempt to update data */ -export type AsyncResourceState = +export type AsyncResourceState = | UninitialisedResourceState | LoadingResourceState | LoadedResourceState @@ -106,6 +106,13 @@ export const isFailedResourceState = ( state: Immutable> ): state is Immutable> => state.type === 'FailedResourceState'; +export const isStaleResourceState = ( + state: Immutable> +): state is Immutable> => + isUninitialisedResourceState(state) || + isLoadedResourceState(state) || + isFailedResourceState(state); + // Set of functions to work with AsyncResourceState export const getLastLoadedResourceState = ( diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts index 071557ec1a815..4c38ac0c4239a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ServerApiError } from '../../../../common/types'; import { NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types/trusted_apps'; import { AsyncResourceState } from '.'; import { TrustedAppsUrlParams } from '../types'; -import { ServerApiError } from '../../../../common/types'; export interface PaginationInfo { index: number; @@ -18,6 +18,7 @@ export interface TrustedAppsListData { items: TrustedApp[]; totalItemsCount: number; paginationInfo: PaginationInfo; + timestamp: number; } /** Store State when an API request has been sent to create a new trusted app entry */ @@ -42,8 +43,14 @@ export interface TrustedAppsListPageState { listView: { currentListResourceState: AsyncResourceState; currentPaginationInfo: PaginationInfo; + freshDataTimestamp: number; show: TrustedAppsUrlParams['show'] | undefined; }; + deletionDialog: { + entry?: TrustedApp; + confirmed: boolean; + submissionResourceState: AsyncResourceState; + }; createView: | undefined | TrustedAppCreatePending diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts index 3a43ffe58262c..5315087c09655 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Action } from 'redux'; + +import { TrustedApp } from '../../../../../common/endpoint/types'; import { AsyncResourceState, TrustedAppCreateFailure, @@ -12,12 +15,30 @@ import { TrustedAppsListData, } from '../state'; -export interface TrustedAppsListResourceStateChanged { - type: 'trustedAppsListResourceStateChanged'; +export type TrustedAppsListDataOutdated = Action<'trustedAppsListDataOutdated'>; + +interface ResourceStateChanged extends Action { + payload: { newState: AsyncResourceState }; +} + +export type TrustedAppsListResourceStateChanged = ResourceStateChanged< + 'trustedAppsListResourceStateChanged', + TrustedAppsListData +>; + +export type TrustedAppDeletionSubmissionResourceStateChanged = ResourceStateChanged< + 'trustedAppDeletionSubmissionResourceStateChanged' +>; + +export type TrustedAppDeletionDialogStarted = Action<'trustedAppDeletionDialogStarted'> & { payload: { - newState: AsyncResourceState; + entry: TrustedApp; }; -} +}; + +export type TrustedAppDeletionDialogConfirmed = Action<'trustedAppDeletionDialogConfirmed'>; + +export type TrustedAppDeletionDialogClosed = Action<'trustedAppDeletionDialogClosed'>; export interface UserClickedSaveNewTrustedAppButton { type: 'userClickedSaveNewTrustedAppButton'; @@ -35,7 +56,12 @@ export interface ServerReturnedCreateTrustedAppFailure { } export type TrustedAppsPageAction = + | TrustedAppsListDataOutdated | TrustedAppsListResourceStateChanged + | TrustedAppDeletionSubmissionResourceStateChanged + | TrustedAppDeletionDialogStarted + | TrustedAppDeletionDialogConfirmed + | TrustedAppDeletionDialogClosed | UserClickedSaveNewTrustedAppButton | ServerReturnedCreateTrustedAppSuccess | ServerReturnedCreateTrustedAppFailure; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts index e5f00ee0ccf81..19c2d3a62781f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts @@ -10,8 +10,10 @@ import { createSpyMiddleware } from '../../../../common/store/test_utils'; import { createFailedListViewWithPagination, + createListLoadedResourceState, createLoadedListViewWithPagination, createLoadingListViewWithPagination, + createSampleTrustedApp, createSampleTrustedApps, createServerApiError, createUserChangedUrlAction, @@ -22,6 +24,14 @@ import { PaginationInfo, TrustedAppsListPageState } from '../state'; import { initialTrustedAppsPageState, trustedAppsPageReducer } from './reducer'; import { createTrustedAppsPageMiddleware } from './middleware'; +const initialNow = 111111; +const dateNowMock = jest.fn(); +dateNowMock.mockReturnValue(initialNow); + +Date.now = dateNowMock; + +const initialState = initialTrustedAppsPageState(); + const createGetTrustedListAppsResponse = (pagination: PaginationInfo, totalItemsCount: number) => ({ data: createSampleTrustedApps(pagination), page: pagination.index, @@ -31,6 +41,7 @@ const createGetTrustedListAppsResponse = (pagination: PaginationInfo, totalItems const createTrustedAppsServiceMock = (): jest.Mocked => ({ getTrustedAppsList: jest.fn(), + deleteTrustedApp: jest.fn(), createTrustedApp: jest.fn(), }); @@ -50,13 +61,19 @@ const createStoreSetup = (trustedAppsService: TrustedAppsService) => { }; describe('middleware', () => { - describe('refreshing list resource state', () => { + beforeEach(() => { + dateNowMock.mockReturnValue(initialNow); + }); + + describe('initial state', () => { it('sets initial state properly', async () => { expect(createStoreSetup(createTrustedAppsServiceMock()).store.getState()).toStrictEqual( - initialTrustedAppsPageState + initialState ); }); + }); + describe('refreshing list resource state', () => { it('refreshes the list when location changes and data gets outdated', async () => { const pagination = { index: 2, size: 50 }; const service = createTrustedAppsServiceMock(); @@ -69,17 +86,17 @@ describe('middleware', () => { store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50')); expect(store.getState()).toStrictEqual({ - listView: createLoadingListViewWithPagination(pagination), + ...initialState, + listView: createLoadingListViewWithPagination(initialNow, pagination), active: true, - createView: undefined, }); await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); expect(store.getState()).toStrictEqual({ - listView: createLoadedListViewWithPagination(pagination, pagination, 500), + ...initialState, + listView: createLoadedListViewWithPagination(initialNow, pagination, pagination, 500), active: true, - createView: undefined, }); }); @@ -100,13 +117,50 @@ describe('middleware', () => { expect(service.getTrustedAppsList).toBeCalledTimes(1); expect(store.getState()).toStrictEqual({ - listView: createLoadedListViewWithPagination(pagination, pagination, 500), + ...initialState, + listView: createLoadedListViewWithPagination(initialNow, pagination, pagination, 500), active: true, - createView: undefined, }); }); - it('set list resource state to faile when failing to load data', async () => { + it('refreshes the list when data gets outdated with and outdate action', async () => { + const newNow = 222222; + const pagination = { index: 0, size: 10 }; + const service = createTrustedAppsServiceMock(); + const { store, spyMiddleware } = createStoreSetup(service); + + service.getTrustedAppsList.mockResolvedValue( + createGetTrustedListAppsResponse(pagination, 500) + ); + + store.dispatch(createUserChangedUrlAction('/trusted_apps')); + + await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); + + dateNowMock.mockReturnValue(newNow); + + store.dispatch({ type: 'trustedAppsListDataOutdated' }); + + expect(store.getState()).toStrictEqual({ + ...initialState, + listView: createLoadingListViewWithPagination( + newNow, + pagination, + createListLoadedResourceState(pagination, 500, initialNow) + ), + active: true, + }); + + await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); + + expect(store.getState()).toStrictEqual({ + ...initialState, + listView: createLoadedListViewWithPagination(newNow, pagination, pagination, 500), + active: true, + }); + }); + + it('set list resource state to failed when failing to load data', async () => { const service = createTrustedAppsServiceMock(); const { store, spyMiddleware } = createStoreSetup(service); @@ -117,12 +171,13 @@ describe('middleware', () => { await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); expect(store.getState()).toStrictEqual({ + ...initialState, listView: createFailedListViewWithPagination( + initialNow, { index: 2, size: 50 }, createServerApiError('Internal Server Error') ), active: true, - createView: undefined, }); const infiniteLoopTest = async () => { @@ -132,4 +187,151 @@ describe('middleware', () => { await expect(infiniteLoopTest).rejects.not.toBeNull(); }); }); + + describe('submitting deletion dialog', () => { + const newNow = 222222; + const entry = createSampleTrustedApp(3); + const notFoundError = createServerApiError('Not Found'); + const pagination = { index: 0, size: 10 }; + const getTrustedAppsListResponse = createGetTrustedListAppsResponse(pagination, 500); + const listView = createLoadedListViewWithPagination(initialNow, pagination, pagination, 500); + const listViewNew = createLoadedListViewWithPagination(newNow, pagination, pagination, 500); + const testStartState = { ...initialState, listView, active: true }; + + it('does not submit when entry is undefined', async () => { + const service = createTrustedAppsServiceMock(); + const { store, spyMiddleware } = createStoreSetup(service); + + service.getTrustedAppsList.mockResolvedValue(getTrustedAppsListResponse); + service.deleteTrustedApp.mockResolvedValue(); + + store.dispatch(createUserChangedUrlAction('/trusted_apps')); + + await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); + + store.dispatch({ type: 'trustedAppDeletionDialogConfirmed' }); + + expect(store.getState()).toStrictEqual({ + ...testStartState, + deletionDialog: { ...testStartState.deletionDialog, confirmed: true }, + }); + }); + + it('submits successfully when entry is defined', async () => { + const service = createTrustedAppsServiceMock(); + const { store, spyMiddleware } = createStoreSetup(service); + + service.getTrustedAppsList.mockResolvedValue(getTrustedAppsListResponse); + service.deleteTrustedApp.mockResolvedValue(); + + store.dispatch(createUserChangedUrlAction('/trusted_apps')); + + await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); + + dateNowMock.mockReturnValue(newNow); + + store.dispatch({ type: 'trustedAppDeletionDialogStarted', payload: { entry } }); + store.dispatch({ type: 'trustedAppDeletionDialogConfirmed' }); + + expect(store.getState()).toStrictEqual({ + ...testStartState, + deletionDialog: { + entry, + confirmed: true, + submissionResourceState: { + type: 'LoadingResourceState', + previousState: { type: 'UninitialisedResourceState' }, + }, + }, + }); + + await spyMiddleware.waitForAction('trustedAppDeletionSubmissionResourceStateChanged'); + await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); + + expect(store.getState()).toStrictEqual({ ...testStartState, listView: listViewNew }); + expect(service.deleteTrustedApp).toBeCalledWith({ id: '3' }); + expect(service.deleteTrustedApp).toBeCalledTimes(1); + }); + + it('does not submit twice', async () => { + const service = createTrustedAppsServiceMock(); + const { store, spyMiddleware } = createStoreSetup(service); + + service.getTrustedAppsList.mockResolvedValue(getTrustedAppsListResponse); + service.deleteTrustedApp.mockResolvedValue(); + + store.dispatch(createUserChangedUrlAction('/trusted_apps')); + + await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); + + dateNowMock.mockReturnValue(newNow); + + store.dispatch({ type: 'trustedAppDeletionDialogStarted', payload: { entry } }); + store.dispatch({ type: 'trustedAppDeletionDialogConfirmed' }); + store.dispatch({ type: 'trustedAppDeletionDialogConfirmed' }); + + expect(store.getState()).toStrictEqual({ + ...testStartState, + deletionDialog: { + entry, + confirmed: true, + submissionResourceState: { + type: 'LoadingResourceState', + previousState: { type: 'UninitialisedResourceState' }, + }, + }, + }); + + await spyMiddleware.waitForAction('trustedAppDeletionSubmissionResourceStateChanged'); + await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); + + expect(store.getState()).toStrictEqual({ ...testStartState, listView: listViewNew }); + expect(service.deleteTrustedApp).toBeCalledWith({ id: '3' }); + expect(service.deleteTrustedApp).toBeCalledTimes(1); + }); + + it('does not submit when server response with failure', async () => { + const service = createTrustedAppsServiceMock(); + const { store, spyMiddleware } = createStoreSetup(service); + + service.getTrustedAppsList.mockResolvedValue(getTrustedAppsListResponse); + service.deleteTrustedApp.mockRejectedValue(notFoundError); + + store.dispatch(createUserChangedUrlAction('/trusted_apps')); + + await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); + + store.dispatch({ type: 'trustedAppDeletionDialogStarted', payload: { entry } }); + store.dispatch({ type: 'trustedAppDeletionDialogConfirmed' }); + + expect(store.getState()).toStrictEqual({ + ...testStartState, + deletionDialog: { + entry, + confirmed: true, + submissionResourceState: { + type: 'LoadingResourceState', + previousState: { type: 'UninitialisedResourceState' }, + }, + }, + }); + + await spyMiddleware.waitForAction('trustedAppDeletionSubmissionResourceStateChanged'); + + expect(store.getState()).toStrictEqual({ + ...testStartState, + deletionDialog: { + entry, + confirmed: true, + submissionResourceState: { + type: 'FailedResourceState', + error: notFoundError, + lastLoadedState: undefined, + }, + }, + }); + expect(service.deleteTrustedApp).toBeCalledWith({ id: '3' }); + expect(service.deleteTrustedApp).toBeCalledTimes(1); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts index bf9cacff5caf0..dd96c8d807048 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts @@ -16,15 +16,22 @@ import { TrustedAppsHttpService, TrustedAppsService } from '../service'; import { AsyncResourceState, + getLastLoadedResourceState, + isStaleResourceState, StaleResourceState, TrustedAppsListData, TrustedAppsListPageState, } from '../state'; -import { TrustedAppsListResourceStateChanged } from './action'; +import { + TrustedAppDeletionSubmissionResourceStateChanged, + TrustedAppsListResourceStateChanged, +} from './action'; import { getCurrentListResourceState, + getDeletionDialogEntry, + getDeletionSubmissionResourceState, getLastLoadedListResourceState, getListCurrentPageIndex, getListCurrentPageSize, @@ -40,46 +47,98 @@ const createTrustedAppsListResourceStateChangedAction = ( payload: { newState }, }); -const refreshList = async ( +const refreshListIfNeeded = async ( store: ImmutableMiddlewareAPI, trustedAppsService: TrustedAppsService ) => { - store.dispatch( - createTrustedAppsListResourceStateChangedAction({ - type: 'LoadingResourceState', - // need to think on how to avoid the casting - previousState: getCurrentListResourceState(store.getState()) as Immutable< - StaleResourceState - >, - }) - ); - - try { - const pageIndex = getListCurrentPageIndex(store.getState()); - const pageSize = getListCurrentPageSize(store.getState()); - const response = await trustedAppsService.getTrustedAppsList({ - page: pageIndex + 1, - per_page: pageSize, - }); - + if (needsRefreshOfListData(store.getState())) { store.dispatch( createTrustedAppsListResourceStateChangedAction({ - type: 'LoadedResourceState', - data: { - items: response.data, - totalItemsCount: response.total, - paginationInfo: { index: pageIndex, size: pageSize }, - }, + type: 'LoadingResourceState', + // need to think on how to avoid the casting + previousState: getCurrentListResourceState(store.getState()) as Immutable< + StaleResourceState + >, }) ); - } catch (error) { + + try { + const pageIndex = getListCurrentPageIndex(store.getState()); + const pageSize = getListCurrentPageSize(store.getState()); + const response = await trustedAppsService.getTrustedAppsList({ + page: pageIndex + 1, + per_page: pageSize, + }); + + store.dispatch( + createTrustedAppsListResourceStateChangedAction({ + type: 'LoadedResourceState', + data: { + items: response.data, + totalItemsCount: response.total, + paginationInfo: { index: pageIndex, size: pageSize }, + timestamp: Date.now(), + }, + }) + ); + } catch (error) { + store.dispatch( + createTrustedAppsListResourceStateChangedAction({ + type: 'FailedResourceState', + error, + lastLoadedState: getLastLoadedListResourceState(store.getState()), + }) + ); + } + } +}; + +const createTrustedAppDeletionSubmissionResourceStateChanged = ( + newState: Immutable +): Immutable => ({ + type: 'trustedAppDeletionSubmissionResourceStateChanged', + payload: { newState }, +}); + +const submitDeletionIfNeeded = async ( + store: ImmutableMiddlewareAPI, + trustedAppsService: TrustedAppsService +) => { + const submissionResourceState = getDeletionSubmissionResourceState(store.getState()); + const entry = getDeletionDialogEntry(store.getState()); + + if (isStaleResourceState(submissionResourceState) && entry !== undefined) { store.dispatch( - createTrustedAppsListResourceStateChangedAction({ - type: 'FailedResourceState', - error, - lastLoadedState: getLastLoadedListResourceState(store.getState()), + createTrustedAppDeletionSubmissionResourceStateChanged({ + type: 'LoadingResourceState', + previousState: submissionResourceState, }) ); + + try { + await trustedAppsService.deleteTrustedApp({ id: entry.id }); + + store.dispatch( + createTrustedAppDeletionSubmissionResourceStateChanged({ + type: 'LoadedResourceState', + data: null, + }) + ); + store.dispatch({ + type: 'trustedAppDeletionDialogClosed', + }); + store.dispatch({ + type: 'trustedAppsListDataOutdated', + }); + } catch (error) { + store.dispatch( + createTrustedAppDeletionSubmissionResourceStateChanged({ + type: 'FailedResourceState', + error, + lastLoadedState: getLastLoadedResourceState(submissionResourceState), + }) + ); + } } }; @@ -102,7 +161,9 @@ const createTrustedApp = async ( data: createdTrustedApp, }, }); - refreshList(store, trustedAppsService); + store.dispatch({ + type: 'trustedAppsListDataOutdated', + }); } catch (error) { dispatch({ type: 'serverReturnedCreateTrustedAppFailure', @@ -122,8 +183,12 @@ export const createTrustedAppsPageMiddleware = ( next(action); // TODO: need to think if failed state is a good condition to consider need for refresh - if (action.type === 'userChangedUrl' && needsRefreshOfListData(store.getState())) { - await refreshList(store, trustedAppsService); + if (action.type === 'userChangedUrl' || action.type === 'trustedAppsListDataOutdated') { + await refreshListIfNeeded(store, trustedAppsService); + } + + if (action.type === 'trustedAppDeletionDialogConfirmed') { + await submitDeletionIfNeeded(store, trustedAppsService); } if (action.type === 'userClickedSaveNewTrustedAppButton') { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts index 76dd4b48e63d2..228f0932edd28 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts @@ -4,93 +4,180 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AsyncResourceState } from '../state'; import { initialTrustedAppsPageState, trustedAppsPageReducer } from './reducer'; import { + createSampleTrustedApp, createListLoadedResourceState, createLoadedListViewWithPagination, - createTrustedAppsListResourceStateChangedAction, createUserChangedUrlAction, + createTrustedAppsListResourceStateChangedAction, } from '../test_utils'; +const initialNow = 111111; +const dateNowMock = jest.fn(); +dateNowMock.mockReturnValue(initialNow); + +Date.now = dateNowMock; + +const initialState = initialTrustedAppsPageState(); + describe('reducer', () => { describe('UserChangedUrl', () => { it('makes page state active and extracts pagination parameters', () => { const result = trustedAppsPageReducer( - initialTrustedAppsPageState, + initialState, createUserChangedUrlAction('/trusted_apps', '?page_index=5&page_size=50') ); expect(result).toStrictEqual({ - listView: { - ...initialTrustedAppsPageState.listView, - currentPaginationInfo: { index: 5, size: 50 }, - }, + ...initialState, + listView: { ...initialState.listView, currentPaginationInfo: { index: 5, size: 50 } }, active: true, - createView: undefined, }); }); it('extracts default pagination parameters when none provided', () => { const result = trustedAppsPageReducer( { - ...initialTrustedAppsPageState, - listView: { - ...initialTrustedAppsPageState.listView, - currentPaginationInfo: { index: 5, size: 50 }, - }, + ...initialState, + listView: { ...initialState.listView, currentPaginationInfo: { index: 5, size: 50 } }, }, createUserChangedUrlAction('/trusted_apps', '?page_index=b&page_size=60') ); - expect(result).toStrictEqual({ - ...initialTrustedAppsPageState, - active: true, - }); + expect(result).toStrictEqual({ ...initialState, active: true }); }); it('extracts default pagination parameters when invalid provided', () => { const result = trustedAppsPageReducer( { - ...initialTrustedAppsPageState, - listView: { - ...initialTrustedAppsPageState.listView, - currentPaginationInfo: { index: 5, size: 50 }, - }, + ...initialState, + listView: { ...initialState.listView, currentPaginationInfo: { index: 5, size: 50 } }, }, createUserChangedUrlAction('/trusted_apps') ); - expect(result).toStrictEqual({ - ...initialTrustedAppsPageState, - active: true, - }); + expect(result).toStrictEqual({ ...initialState, active: true }); }); it('makes page state inactive and resets list to uninitialised state when navigating away', () => { const result = trustedAppsPageReducer( - { listView: createLoadedListViewWithPagination(), active: true, createView: undefined }, + { ...initialState, listView: createLoadedListViewWithPagination(initialNow), active: true }, createUserChangedUrlAction('/endpoints') ); - expect(result).toStrictEqual(initialTrustedAppsPageState); + expect(result).toStrictEqual(initialState); }); }); describe('TrustedAppsListResourceStateChanged', () => { it('sets the current list resource state', () => { - const listResourceState = createListLoadedResourceState({ index: 3, size: 50 }, 200); + const listResourceState = createListLoadedResourceState( + { index: 3, size: 50 }, + 200, + initialNow + ); const result = trustedAppsPageReducer( - initialTrustedAppsPageState, + initialState, createTrustedAppsListResourceStateChangedAction(listResourceState) ); expect(result).toStrictEqual({ - ...initialTrustedAppsPageState, - listView: { - ...initialTrustedAppsPageState.listView, - currentListResourceState: listResourceState, + ...initialState, + listView: { ...initialState.listView, currentListResourceState: listResourceState }, + }); + }); + }); + + describe('TrustedAppsListDataOutdated', () => { + it('sets the list view freshness timestamp', () => { + const newNow = 222222; + dateNowMock.mockReturnValue(newNow); + + const result = trustedAppsPageReducer(initialState, { type: 'trustedAppsListDataOutdated' }); + + expect(result).toStrictEqual({ + ...initialState, + listView: { ...initialState.listView, freshDataTimestamp: newNow }, + }); + }); + }); + + describe('TrustedAppDeletionSubmissionResourceStateChanged', () => { + it('sets the deletion dialog submission resource state', () => { + const submissionResourceState: AsyncResourceState = { + type: 'LoadedResourceState', + data: null, + }; + const result = trustedAppsPageReducer(initialState, { + type: 'trustedAppDeletionSubmissionResourceStateChanged', + payload: { newState: submissionResourceState }, + }); + + expect(result).toStrictEqual({ + ...initialState, + deletionDialog: { ...initialState.deletionDialog, submissionResourceState }, + }); + }); + }); + + describe('TrustedAppDeletionDialogStarted', () => { + it('sets the deletion dialog state to started', () => { + const entry = createSampleTrustedApp(3); + const result = trustedAppsPageReducer(initialState, { + type: 'trustedAppDeletionDialogStarted', + payload: { entry }, + }); + + expect(result).toStrictEqual({ + ...initialState, + deletionDialog: { ...initialState.deletionDialog, entry }, + }); + }); + }); + + describe('TrustedAppDeletionDialogConfirmed', () => { + it('sets the deletion dialog state to confirmed', () => { + const entry = createSampleTrustedApp(3); + const result = trustedAppsPageReducer( + { + ...initialState, + deletionDialog: { + entry, + confirmed: false, + submissionResourceState: { type: 'UninitialisedResourceState' }, + }, + }, + { type: 'trustedAppDeletionDialogConfirmed' } + ); + + expect(result).toStrictEqual({ + ...initialState, + deletionDialog: { + entry, + confirmed: true, + submissionResourceState: { type: 'UninitialisedResourceState' }, }, }); }); }); + + describe('TrustedAppDeletionDialogClosed', () => { + it('sets the deletion dialog state to confirmed', () => { + const result = trustedAppsPageReducer( + { + ...initialState, + deletionDialog: { + entry: createSampleTrustedApp(3), + confirmed: true, + submissionResourceState: { type: 'UninitialisedResourceState' }, + }, + }, + { type: 'trustedAppDeletionDialogClosed' } + ); + + expect(result).toStrictEqual(initialState); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts index d824a6e95c8d5..ec210254bf76f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts @@ -19,9 +19,14 @@ import { } from '../../../common/constants'; import { + TrustedAppDeletionDialogClosed, + TrustedAppDeletionDialogConfirmed, + TrustedAppDeletionDialogStarted, + TrustedAppDeletionSubmissionResourceStateChanged, + TrustedAppsListDataOutdated, + TrustedAppsListResourceStateChanged, ServerReturnedCreateTrustedAppFailure, ServerReturnedCreateTrustedAppSuccess, - TrustedAppsListResourceStateChanged, UserClickedSaveNewTrustedAppButton, } from './action'; import { TrustedAppsListPageState } from '../state'; @@ -41,6 +46,16 @@ const isTrustedAppsPageLocation = (location: Immutable) => { ); }; +const trustedAppsListDataOutdated: CaseReducer = (state, action) => { + return { + ...state, + listView: { + ...state.listView, + freshDataTimestamp: Date.now(), + }, + }; +}; + const trustedAppsListResourceStateChanged: CaseReducer = ( state, action @@ -54,6 +69,44 @@ const trustedAppsListResourceStateChanged: CaseReducer = ( + state, + action +) => { + return { + ...state, + deletionDialog: { ...state.deletionDialog, submissionResourceState: action.payload.newState }, + }; +}; + +const trustedAppDeletionDialogStarted: CaseReducer = ( + state, + action +) => { + return { + ...state, + deletionDialog: { + entry: action.payload.entry, + confirmed: false, + submissionResourceState: { type: 'UninitialisedResourceState' }, + }, + }; +}; + +const trustedAppDeletionDialogConfirmed: CaseReducer = ( + state, + action +) => { + return { ...state, deletionDialog: { ...state.deletionDialog, confirmed: true } }; +}; + +const trustedAppDeletionDialogClosed: CaseReducer = ( + state, + action +) => { + return { ...state, deletionDialog: initialDeletionDialogState() }; +}; + const userChangedUrl: CaseReducer = (state, action) => { if (isTrustedAppsPageLocation(action.payload)) { const parsedUrlsParams = parse(action.payload.search.slice(1)); @@ -75,7 +128,7 @@ const userChangedUrl: CaseReducer = (state, action) => { active: true, }; } else { - return initialTrustedAppsPageState; + return initialTrustedAppsPageState(); } }; @@ -90,27 +143,49 @@ const trustedAppsCreateResourceChanged: CaseReducer< }; }; -export const initialTrustedAppsPageState: TrustedAppsListPageState = { +const initialDeletionDialogState = (): TrustedAppsListPageState['deletionDialog'] => ({ + confirmed: false, + submissionResourceState: { type: 'UninitialisedResourceState' }, +}); + +export const initialTrustedAppsPageState = (): TrustedAppsListPageState => ({ listView: { currentListResourceState: { type: 'UninitialisedResourceState' }, currentPaginationInfo: { index: MANAGEMENT_DEFAULT_PAGE, size: MANAGEMENT_DEFAULT_PAGE_SIZE, }, + freshDataTimestamp: Date.now(), show: undefined, }, + deletionDialog: initialDeletionDialogState(), createView: undefined, active: false, -}; +}); export const trustedAppsPageReducer: StateReducer = ( - state = initialTrustedAppsPageState, + state = initialTrustedAppsPageState(), action ) => { switch (action.type) { + case 'trustedAppsListDataOutdated': + return trustedAppsListDataOutdated(state, action); + case 'trustedAppsListResourceStateChanged': return trustedAppsListResourceStateChanged(state, action); + case 'trustedAppDeletionSubmissionResourceStateChanged': + return trustedAppDeletionSubmissionResourceStateChanged(state, action); + + case 'trustedAppDeletionDialogStarted': + return trustedAppDeletionDialogStarted(state, action); + + case 'trustedAppDeletionDialogConfirmed': + return trustedAppDeletionDialogConfirmed(state, action); + + case 'trustedAppDeletionDialogClosed': + return trustedAppDeletionDialogClosed(state, action); + case 'userChangedUrl': return userChangedUrl(state, action); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts index 453afa1befa6b..0be4d0b05acc4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AsyncResourceState, TrustedAppsListPageState } from '../state'; +import { initialTrustedAppsPageState } from './reducer'; import { getCurrentListResourceState, getLastLoadedListResourceState, @@ -14,6 +16,12 @@ import { getListTotalItemsCount, isListLoading, needsRefreshOfListData, + isDeletionDialogOpen, + isDeletionInProgress, + isDeletionSuccessful, + getDeletionError, + getDeletionDialogEntry, + getDeletionSubmissionResourceState, } from './selectors'; import { @@ -23,96 +31,118 @@ import { createListFailedResourceState, createListLoadedResourceState, createLoadedListViewWithPagination, + createSampleTrustedApp, createSampleTrustedApps, + createServerApiError, createUninitialisedResourceState, } from '../test_utils'; +const initialNow = 111111; +const dateNowMock = jest.fn(); +dateNowMock.mockReturnValue(initialNow); + +Date.now = dateNowMock; + +const initialState = initialTrustedAppsPageState(); + +const createStateWithDeletionSubmissionResourceState = ( + submissionResourceState: AsyncResourceState +): TrustedAppsListPageState => ({ + ...initialState, + deletionDialog: { ...initialState.deletionDialog, submissionResourceState }, +}); + describe('selectors', () => { describe('needsRefreshOfListData()', () => { it('returns false for outdated resource state and inactive state', () => { - expect( - needsRefreshOfListData({ - listView: createDefaultListView(), - active: false, - createView: undefined, - }) - ).toBe(false); + expect(needsRefreshOfListData(initialState)).toBe(false); }); it('returns true for outdated resource state and active state', () => { - expect( - needsRefreshOfListData({ - listView: createDefaultListView(), - active: true, - createView: undefined, - }) - ).toBe(true); + expect(needsRefreshOfListData({ ...initialState, active: true })).toBe(true); }); it('returns true when current loaded page index is outdated', () => { - const listView = createLoadedListViewWithPagination({ index: 1, size: 20 }); + const listView = createLoadedListViewWithPagination(initialNow, { index: 1, size: 20 }); - expect(needsRefreshOfListData({ listView, active: true, createView: undefined })).toBe(true); + expect(needsRefreshOfListData({ ...initialState, listView, active: true })).toBe(true); }); it('returns true when current loaded page size is outdated', () => { - const listView = createLoadedListViewWithPagination({ index: 0, size: 50 }); + const listView = createLoadedListViewWithPagination(initialNow, { index: 0, size: 50 }); + + expect(needsRefreshOfListData({ ...initialState, listView, active: true })).toBe(true); + }); + + it('returns true when current loaded data timestamp is outdated', () => { + const listView = { + ...createLoadedListViewWithPagination(111111), + freshDataTimestamp: 222222, + }; - expect(needsRefreshOfListData({ listView, active: true, createView: undefined })).toBe(true); + expect(needsRefreshOfListData({ ...initialState, listView, active: true })).toBe(true); }); it('returns false when current loaded data is up to date', () => { - const listView = createLoadedListViewWithPagination(); + const listView = createLoadedListViewWithPagination(initialNow); - expect(needsRefreshOfListData({ listView, active: true, createView: undefined })).toBe(false); + expect(needsRefreshOfListData({ ...initialState, listView, active: true })).toBe(false); }); }); describe('getCurrentListResourceState()', () => { it('returns current list resource state', () => { - const listView = createDefaultListView(); + const state = { ...initialState, listView: createDefaultListView(initialNow) }; - expect( - getCurrentListResourceState({ listView, active: false, createView: undefined }) - ).toStrictEqual(createUninitialisedResourceState()); + expect(getCurrentListResourceState(state)).toStrictEqual(createUninitialisedResourceState()); }); }); describe('getLastLoadedListResourceState()', () => { it('returns last loaded list resource state', () => { - const listView = { - currentListResourceState: createListComplexLoadingResourceState( - createDefaultPaginationInfo(), - 200 - ), - currentPaginationInfo: createDefaultPaginationInfo(), - show: undefined, + const state = { + ...initialState, + listView: { + currentListResourceState: createListComplexLoadingResourceState( + createDefaultPaginationInfo(), + 200, + initialNow + ), + currentPaginationInfo: createDefaultPaginationInfo(), + freshDataTimestamp: initialNow, + show: undefined, + }, }; - expect( - getLastLoadedListResourceState({ listView, active: false, createView: undefined }) - ).toStrictEqual(createListLoadedResourceState(createDefaultPaginationInfo(), 200)); + expect(getLastLoadedListResourceState(state)).toStrictEqual( + createListLoadedResourceState(createDefaultPaginationInfo(), 200, initialNow) + ); }); }); describe('getListItems()', () => { it('returns empty list when no valid data loaded', () => { - expect( - getListItems({ listView: createDefaultListView(), active: false, createView: undefined }) - ).toStrictEqual([]); + const state = { ...initialState, listView: createDefaultListView(initialNow) }; + + expect(getListItems(state)).toStrictEqual([]); }); it('returns last loaded list items', () => { - const listView = { - currentListResourceState: createListComplexLoadingResourceState( - createDefaultPaginationInfo(), - 200 - ), - currentPaginationInfo: createDefaultPaginationInfo(), - show: undefined, + const state = { + ...initialState, + listView: { + currentListResourceState: createListComplexLoadingResourceState( + createDefaultPaginationInfo(), + 200, + initialNow + ), + currentPaginationInfo: createDefaultPaginationInfo(), + freshDataTimestamp: initialNow, + show: undefined, + }, }; - expect(getListItems({ listView, active: false, createView: undefined })).toStrictEqual( + expect(getListItems(state)).toStrictEqual( createSampleTrustedApps(createDefaultPaginationInfo()) ); }); @@ -120,100 +150,239 @@ describe('selectors', () => { describe('getListTotalItemsCount()', () => { it('returns 0 when no valid data loaded', () => { - expect( - getListTotalItemsCount({ - listView: createDefaultListView(), - active: false, - createView: undefined, - }) - ).toBe(0); + const state = { ...initialState, listView: createDefaultListView(initialNow) }; + + expect(getListTotalItemsCount(state)).toBe(0); }); it('returns last loaded total items count', () => { - const listView = { - currentListResourceState: createListComplexLoadingResourceState( - createDefaultPaginationInfo(), - 200 - ), - currentPaginationInfo: createDefaultPaginationInfo(), - show: undefined, + const state = { + ...initialState, + listView: { + currentListResourceState: createListComplexLoadingResourceState( + createDefaultPaginationInfo(), + 200, + initialNow + ), + currentPaginationInfo: createDefaultPaginationInfo(), + freshDataTimestamp: initialNow, + show: undefined, + }, }; - expect(getListTotalItemsCount({ listView, active: false, createView: undefined })).toBe(200); + expect(getListTotalItemsCount(state)).toBe(200); }); }); describe('getListCurrentPageIndex()', () => { it('returns page index', () => { - expect( - getListCurrentPageIndex({ - listView: createDefaultListView(), - active: false, - createView: undefined, - }) - ).toBe(0); + const state = { ...initialState, listView: createDefaultListView(initialNow) }; + + expect(getListCurrentPageIndex(state)).toBe(0); }); }); describe('getListCurrentPageSize()', () => { - it('returns page index', () => { - expect( - getListCurrentPageSize({ - listView: createDefaultListView(), - active: false, - createView: undefined, - }) - ).toBe(20); + it('returns page size', () => { + const state = { ...initialState, listView: createDefaultListView(initialNow) }; + + expect(getListCurrentPageSize(state)).toBe(20); }); }); describe('getListErrorMessage()', () => { it('returns undefined when not in failed state', () => { - const listView = { - currentListResourceState: createListComplexLoadingResourceState( - createDefaultPaginationInfo(), - 200 - ), - currentPaginationInfo: createDefaultPaginationInfo(), - show: undefined, + const state = { + ...initialState, + listView: { + currentListResourceState: createListComplexLoadingResourceState( + createDefaultPaginationInfo(), + 200, + initialNow + ), + currentPaginationInfo: createDefaultPaginationInfo(), + freshDataTimestamp: initialNow, + show: undefined, + }, }; - expect( - getListErrorMessage({ listView, active: false, createView: undefined }) - ).toBeUndefined(); + expect(getListErrorMessage(state)).toBeUndefined(); }); it('returns message when not in failed state', () => { - const listView = { - currentListResourceState: createListFailedResourceState('Internal Server Error'), - currentPaginationInfo: createDefaultPaginationInfo(), - show: undefined, + const state = { + ...initialState, + listView: { + currentListResourceState: createListFailedResourceState('Internal Server Error'), + currentPaginationInfo: createDefaultPaginationInfo(), + freshDataTimestamp: initialNow, + show: undefined, + }, }; - expect(getListErrorMessage({ listView, active: false, createView: undefined })).toBe( - 'Internal Server Error' - ); + expect(getListErrorMessage(state)).toBe('Internal Server Error'); }); }); describe('isListLoading()', () => { it('returns false when no loading is happening', () => { - expect( - isListLoading({ listView: createDefaultListView(), active: false, createView: undefined }) - ).toBe(false); + expect(isListLoading(initialState)).toBe(false); }); it('returns true when loading is in progress', () => { - const listView = { - currentListResourceState: createListComplexLoadingResourceState( - createDefaultPaginationInfo(), - 200 - ), - currentPaginationInfo: createDefaultPaginationInfo(), - show: undefined, + const state = { + ...initialState, + listView: { + currentListResourceState: createListComplexLoadingResourceState( + createDefaultPaginationInfo(), + 200, + initialNow + ), + currentPaginationInfo: createDefaultPaginationInfo(), + freshDataTimestamp: initialNow, + show: undefined, + }, }; - expect(isListLoading({ listView, active: false, createView: undefined })).toBe(true); + expect(isListLoading(state)).toBe(true); + }); + }); + + describe('isDeletionDialogOpen()', () => { + it('returns false when no entry is set', () => { + expect(isDeletionDialogOpen(initialState)).toBe(false); + }); + + it('returns true when entry is set', () => { + const state = { + ...initialState, + deletionDialog: { + ...initialState.deletionDialog, + entry: createSampleTrustedApp(5), + }, + }; + + expect(isDeletionDialogOpen(state)).toBe(true); + }); + }); + + describe('isDeletionInProgress()', () => { + it('returns false when resource state is uninitialised', () => { + expect(isDeletionInProgress(initialState)).toBe(false); + }); + + it('returns true when resource state is loading', () => { + const state = createStateWithDeletionSubmissionResourceState({ + type: 'LoadingResourceState', + previousState: { type: 'UninitialisedResourceState' }, + }); + + expect(isDeletionInProgress(state)).toBe(true); + }); + + it('returns false when resource state is loaded', () => { + const state = createStateWithDeletionSubmissionResourceState({ + type: 'LoadedResourceState', + data: null, + }); + + expect(isDeletionInProgress(state)).toBe(false); + }); + + it('returns false when resource state is failed', () => { + const state = createStateWithDeletionSubmissionResourceState({ + type: 'FailedResourceState', + error: createServerApiError('Not Found'), + }); + + expect(isDeletionInProgress(state)).toBe(false); + }); + }); + + describe('isDeletionSuccessful()', () => { + it('returns false when resource state is uninitialised', () => { + expect(isDeletionSuccessful(initialState)).toBe(false); + }); + + it('returns false when resource state is loading', () => { + const state = createStateWithDeletionSubmissionResourceState({ + type: 'LoadingResourceState', + previousState: { type: 'UninitialisedResourceState' }, + }); + + expect(isDeletionSuccessful(state)).toBe(false); + }); + + it('returns true when resource state is loaded', () => { + const state = createStateWithDeletionSubmissionResourceState({ + type: 'LoadedResourceState', + data: null, + }); + + expect(isDeletionSuccessful(state)).toBe(true); + }); + + it('returns false when resource state is failed', () => { + const state = createStateWithDeletionSubmissionResourceState({ + type: 'FailedResourceState', + error: createServerApiError('Not Found'), + }); + + expect(isDeletionSuccessful(state)).toBe(false); + }); + }); + + describe('getDeletionError()', () => { + it('returns undefined when resource state is uninitialised', () => { + expect(getDeletionError(initialState)).toBeUndefined(); + }); + + it('returns undefined when resource state is loading', () => { + const state = createStateWithDeletionSubmissionResourceState({ + type: 'LoadingResourceState', + previousState: { type: 'UninitialisedResourceState' }, + }); + + expect(getDeletionError(state)).toBeUndefined(); + }); + + it('returns undefined when resource state is loaded', () => { + const state = createStateWithDeletionSubmissionResourceState({ + type: 'LoadedResourceState', + data: null, + }); + + expect(getDeletionError(state)).toBeUndefined(); + }); + + it('returns error when resource state is failed', () => { + const state = createStateWithDeletionSubmissionResourceState({ + type: 'FailedResourceState', + error: createServerApiError('Not Found'), + }); + + expect(getDeletionError(state)).toStrictEqual(createServerApiError('Not Found')); + }); + }); + + describe('getDeletionSubmissionResourceState()', () => { + it('returns submission resource state', () => { + expect(getDeletionSubmissionResourceState(initialState)).toStrictEqual({ + type: 'UninitialisedResourceState', + }); + }); + }); + + describe('getDeletionDialogEntry()', () => { + it('returns undefined when no entry is set', () => { + expect(getDeletionDialogEntry(initialState)).toBeUndefined(); + }); + + it('returns entry when entry is set', () => { + const entry = createSampleTrustedApp(5); + const state = { ...initialState, deletionDialog: { ...initialState.deletionDialog, entry } }; + + expect(getDeletionDialogEntry(state)).toStrictEqual(entry); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts index f074b21f79f4e..6239b425efe2f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts @@ -5,12 +5,15 @@ */ import { createSelector } from 'reselect'; +import { ServerApiError } from '../../../../common/types'; import { Immutable, NewTrustedApp, TrustedApp } from '../../../../../common/endpoint/types'; import { AsyncResourceState, getCurrentResourceError, getLastLoadedResourceState, + isFailedResourceState, + isLoadedResourceState, isLoadingResourceState, isOutdatedResourceState, LoadedResourceState, @@ -32,12 +35,15 @@ const pageInfosEqual = (pageInfo1: PaginationInfo, pageInfo2: PaginationInfo): b export const needsRefreshOfListData = (state: Immutable): boolean => { const currentPageInfo = state.listView.currentPaginationInfo; const currentPage = state.listView.currentListResourceState; + const freshDataTimestamp = state.listView.freshDataTimestamp; return ( state.active && - isOutdatedResourceState(currentPage, (data) => - pageInfosEqual(currentPageInfo, data.paginationInfo) - ) + isOutdatedResourceState(currentPage, (data) => { + return ( + pageInfosEqual(currentPageInfo, data.paginationInfo) && data.timestamp >= freshDataTimestamp + ); + }) ); }; @@ -104,6 +110,38 @@ export const isListLoading = (state: Immutable): boole return isLoadingResourceState(state.listView.currentListResourceState); }; +export const isDeletionDialogOpen = (state: Immutable): boolean => { + return state.deletionDialog.entry !== undefined; +}; + +export const isDeletionInProgress = (state: Immutable): boolean => { + return isLoadingResourceState(state.deletionDialog.submissionResourceState); +}; + +export const isDeletionSuccessful = (state: Immutable): boolean => { + return isLoadedResourceState(state.deletionDialog.submissionResourceState); +}; + +export const getDeletionError = ( + state: Immutable +): Immutable | undefined => { + const submissionResourceState = state.deletionDialog.submissionResourceState; + + return isFailedResourceState(submissionResourceState) ? submissionResourceState.error : undefined; +}; + +export const getDeletionSubmissionResourceState = ( + state: Immutable +): AsyncResourceState => { + return state.deletionDialog.submissionResourceState; +}; + +export const getDeletionDialogEntry = ( + state: Immutable +): Immutable | undefined => { + return state.deletionDialog.entry; +}; + export const isCreatePending: (state: Immutable) => boolean = ({ createView, }) => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts index 70e4e1e685b01..020a87f526e52 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts @@ -4,10 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import { combineReducers, createStore } from 'redux'; import { ServerApiError } from '../../../../common/types'; import { TrustedApp } from '../../../../../common/endpoint/types'; import { RoutingAction } from '../../../../common/store/routing'; +import { + MANAGEMENT_STORE_GLOBAL_NAMESPACE, + MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE, +} from '../../../common/constants'; + import { AsyncResourceState, FailedResourceState, @@ -20,30 +26,36 @@ import { UninitialisedResourceState, } from '../state'; +import { trustedAppsPageReducer } from '../store/reducer'; import { TrustedAppsListResourceStateChanged } from '../store/action'; -import { initialTrustedAppsPageState } from '../store/reducer'; const OS_LIST: Array = ['windows', 'macos', 'linux']; -export const createSampleTrustedApps = (paginationInfo: PaginationInfo): TrustedApp[] => { - return [...new Array(paginationInfo.size).keys()].map((i) => ({ - id: String(paginationInfo.index + i), - name: `trusted app ${paginationInfo.index + i}`, - description: `Trusted App ${paginationInfo.index + i}`, +export const createSampleTrustedApp = (i: number): TrustedApp => { + return { + id: String(i), + name: `trusted app ${i}`, + description: `Trusted App ${i}`, created_at: '1 minute ago', created_by: 'someone', os: OS_LIST[i % 3], entries: [], - })); + }; +}; + +export const createSampleTrustedApps = (paginationInfo: PaginationInfo): TrustedApp[] => { + return [...new Array(paginationInfo.size).keys()].map(createSampleTrustedApp); }; export const createTrustedAppsListData = ( paginationInfo: PaginationInfo, - totalItemsCount: number + totalItemsCount: number, + timestamp: number ) => ({ items: createSampleTrustedApps(paginationInfo), totalItemsCount, paginationInfo, + timestamp, }); export const createServerApiError = (message: string) => ({ @@ -58,10 +70,11 @@ export const createUninitialisedResourceState = (): UninitialisedResourceState = export const createListLoadedResourceState = ( paginationInfo: PaginationInfo, - totalItemsCount: number + totalItemsCount: number, + timestamp: number ): LoadedResourceState => ({ type: 'LoadedResourceState', - data: createTrustedAppsListData(paginationInfo, totalItemsCount), + data: createTrustedAppsListData(paginationInfo, totalItemsCount, timestamp), }); export const createListFailedResourceState = ( @@ -82,50 +95,64 @@ export const createListLoadingResourceState = ( export const createListComplexLoadingResourceState = ( paginationInfo: PaginationInfo, - totalItemsCount: number + totalItemsCount: number, + timestamp: number ): LoadingResourceState => createListLoadingResourceState( createListFailedResourceState( 'Internal Server Error', - createListLoadedResourceState(paginationInfo, totalItemsCount) + createListLoadedResourceState(paginationInfo, totalItemsCount, timestamp) ) ); export const createDefaultPaginationInfo = () => ({ index: 0, size: 20 }); -export const createDefaultListView = () => ({ - ...initialTrustedAppsPageState.listView, +export const createDefaultListView = ( + freshDataTimestamp: number +): TrustedAppsListPageState['listView'] => ({ currentListResourceState: createUninitialisedResourceState(), currentPaginationInfo: createDefaultPaginationInfo(), + freshDataTimestamp, + show: undefined, }); export const createLoadingListViewWithPagination = ( + freshDataTimestamp: number, currentPaginationInfo: PaginationInfo, previousState: StaleResourceState = createUninitialisedResourceState() ): TrustedAppsListPageState['listView'] => ({ - ...initialTrustedAppsPageState.listView, currentListResourceState: { type: 'LoadingResourceState', previousState }, currentPaginationInfo, + freshDataTimestamp, + show: undefined, }); export const createLoadedListViewWithPagination = ( + freshDataTimestamp: number, paginationInfo: PaginationInfo = createDefaultPaginationInfo(), currentPaginationInfo: PaginationInfo = createDefaultPaginationInfo(), totalItemsCount: number = 200 ): TrustedAppsListPageState['listView'] => ({ - ...initialTrustedAppsPageState.listView, - currentListResourceState: createListLoadedResourceState(paginationInfo, totalItemsCount), + currentListResourceState: createListLoadedResourceState( + paginationInfo, + totalItemsCount, + freshDataTimestamp + ), currentPaginationInfo, + freshDataTimestamp, + show: undefined, }); export const createFailedListViewWithPagination = ( + freshDataTimestamp: number, currentPaginationInfo: PaginationInfo, error: ServerApiError, lastLoadedState?: LoadedResourceState ): TrustedAppsListPageState['listView'] => ({ - ...initialTrustedAppsPageState.listView, currentListResourceState: { type: 'FailedResourceState', error, lastLoadedState }, currentPaginationInfo, + freshDataTimestamp, + show: undefined, }); export const createUserChangedUrlAction = (path: string, search: string = ''): RoutingAction => { @@ -138,3 +165,13 @@ export const createTrustedAppsListResourceStateChangedAction = ( type: 'trustedAppsListResourceStateChanged', payload: { newState }, }); + +export const createGlobalNoMiddlewareStore = () => { + return createStore( + combineReducers({ + [MANAGEMENT_STORE_GLOBAL_NAMESPACE]: combineReducers({ + [MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: trustedAppsPageReducer, + }), + }) + ); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_app_deletion_dialog.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_app_deletion_dialog.test.tsx.snap new file mode 100644 index 0000000000000..fdb20f229f144 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_app_deletion_dialog.test.tsx.snap @@ -0,0 +1,315 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TrustedAppDeletionDialog renders correctly initially 1`] = ` + +
+ +`; + +exports[`TrustedAppDeletionDialog renders correctly when deletion failed 1`] = ` + +
+
+
+
+ +
+
+
+ Remove trusted application +
+
+
+
+
+

+ You are removing trusted application " + + trusted app 3 + + ". +

+

+ This action cannot be undone. Are you sure you wish to continue? +

+
+
+
+
+ + +
+
+
+
+
+ +`; + +exports[`TrustedAppDeletionDialog renders correctly when deletion is in progress 1`] = ` + +
+
+
+
+ +
+
+
+ Remove trusted application +
+
+
+
+
+

+ You are removing trusted application " + + trusted app 3 + + ". +

+

+ This action cannot be undone. Are you sure you wish to continue? +

+
+
+
+
+ + +
+
+
+
+
+ +`; + +exports[`TrustedAppDeletionDialog renders correctly when dialog started 1`] = ` + +
+
+
+
+ +
+
+
+ Remove trusted application +
+
+
+
+
+

+ You are removing trusted application " + + trusted app 3 + + ". +

+

+ This action cannot be undone. Are you sure you wish to continue? +

+
+
+
+
+ + +
+
+
+
+
+ +`; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_list.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_list.test.tsx.snap index e0f846f5950f7..46885bd653dc2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_list.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_list.test.tsx.snap @@ -98,6 +98,22 @@ exports[`TrustedAppsList renders correctly initially 1`] = `
+ +
+ + Actions + +
+ @@ -106,7 +122,7 @@ exports[`TrustedAppsList renders correctly initially 1`] = ` >
+ +
+ + Actions + +
+ @@ -232,7 +264,7 @@ exports[`TrustedAppsList renders correctly when failed loading data for the firs >
+ +
+ + Actions + +
+ @@ -363,7 +411,7 @@ exports[`TrustedAppsList renders correctly when failed loading data for the seco >
+ +
+ + Actions + +
+
+ +
+ + + + Delete + + +
+
+ +
+ + + + Delete + + +
+
+ +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ - -
- Name + + + + Delete + + +
+ + + + +
+ Name
+ +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ @@ -2174,6 +2838,22 @@ exports[`TrustedAppsList renders correctly when loading data for the first time + +
+ + Actions + +
+ @@ -2182,7 +2862,7 @@ exports[`TrustedAppsList renders correctly when loading data for the first time >
+ +
+ + Actions + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ - + +
+ + + + Delete + + +
+ + + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ @@ -3890,7 +5186,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time `; -exports[`TrustedAppsList renders correctly when new page and page sie set (not loading yet) 1`] = ` +exports[`TrustedAppsList renders correctly when new page and page size set (not loading yet) 1`] = `
+ +
+ + Actions + +
+
+ +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap index c8d9b46d5a0d2..9e0a6b16f8b8a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap @@ -263,6 +263,22 @@ Object { + +
+ + Actions + +
+ @@ -271,7 +287,7 @@ Object { >
+ +
+ + Actions + +
+ @@ -496,7 +528,7 @@ Object { >
) => { + const Wrapper: React.FC = ({ children }) => ( + + {children} + + ); + + return render(, { wrapper: Wrapper }); +}; + +const createDialogStartAction = (): TrustedAppDeletionDialogStarted => ({ + type: 'trustedAppDeletionDialogStarted', + payload: { entry: createSampleTrustedApp(3) }, +}); + +const createDialogLoadingAction = (): TrustedAppDeletionSubmissionResourceStateChanged => ({ + type: 'trustedAppDeletionSubmissionResourceStateChanged', + payload: { + newState: { + type: 'LoadingResourceState', + previousState: { type: 'UninitialisedResourceState' }, + }, + }, +}); + +const createDialogFailedAction = (): TrustedAppDeletionSubmissionResourceStateChanged => ({ + type: 'trustedAppDeletionSubmissionResourceStateChanged', + payload: { + newState: { type: 'FailedResourceState', error: createServerApiError('Not Found') }, + }, +}); + +describe('TrustedAppDeletionDialog', () => { + it('renders correctly initially', () => { + expect(renderDeletionDialog(createGlobalNoMiddlewareStore()).baseElement).toMatchSnapshot(); + }); + + it('renders correctly when dialog started', () => { + const store = createGlobalNoMiddlewareStore(); + + store.dispatch(createDialogStartAction()); + + expect(renderDeletionDialog(store).baseElement).toMatchSnapshot(); + }); + + it('renders correctly when deletion is in progress', () => { + const store = createGlobalNoMiddlewareStore(); + + store.dispatch(createDialogStartAction()); + store.dispatch(createDialogLoadingAction()); + + expect(renderDeletionDialog(store).baseElement).toMatchSnapshot(); + }); + + it('renders correctly when deletion failed', () => { + const store = createGlobalNoMiddlewareStore(); + + store.dispatch(createDialogStartAction()); + store.dispatch(createDialogFailedAction()); + + expect(renderDeletionDialog(store).baseElement).toMatchSnapshot(); + }); + + it('triggers confirmation action when confirm button clicked', async () => { + const store = createGlobalNoMiddlewareStore(); + + store.dispatch(createDialogStartAction()); + store.dispatch = jest.fn(); + + (await renderDeletionDialog(store).findByTestId('trustedAppDeletionConfirm')).click(); + + expect(store.dispatch).toBeCalledWith({ + type: 'trustedAppDeletionDialogConfirmed', + }); + }); + + it('triggers closing action when cancel button clicked', async () => { + const store = createGlobalNoMiddlewareStore(); + + store.dispatch(createDialogStartAction()); + store.dispatch = jest.fn(); + + (await renderDeletionDialog(store).findByTestId('trustedAppDeletionCancel')).click(); + + expect(store.dispatch).toBeCalledWith({ + type: 'trustedAppDeletionDialogClosed', + }); + }); + + it('does not trigger closing action when deletion in progress and cancel button clicked', async () => { + const store = createGlobalNoMiddlewareStore(); + + store.dispatch(createDialogStartAction()); + store.dispatch(createDialogLoadingAction()); + + store.dispatch = jest.fn(); + + (await renderDeletionDialog(store).findByTestId('trustedAppDeletionCancel')).click(); + + expect(store.dispatch).not.toBeCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx new file mode 100644 index 0000000000000..846fa794ceefd --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import { Dispatch } from 'redux'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButton, + EuiButtonEmpty, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, + EuiText, +} from '@elastic/eui'; + +import { Immutable, TrustedApp } from '../../../../../common/endpoint/types'; +import { AppAction } from '../../../../common/store/actions'; +import { useTrustedAppsSelector } from './hooks'; +import { + getDeletionDialogEntry, + isDeletionDialogOpen, + isDeletionInProgress, +} from '../store/selectors'; + +const CANCEL_SUBJ = 'trustedAppDeletionCancel'; +const CONFIRM_SUBJ = 'trustedAppDeletionConfirm'; + +const getTranslations = (entry: Immutable | undefined) => ({ + title: ( + + ), + mainMessage: ( + {entry?.name} }} + /> + ), + subMessage: ( + + ), + cancelButton: ( + + ), + confirmButton: ( + + ), +}); + +export const TrustedAppDeletionDialog = memo(() => { + const dispatch = useDispatch>(); + const isBusy = useTrustedAppsSelector(isDeletionInProgress); + const entry = useTrustedAppsSelector(getDeletionDialogEntry); + const translations = useMemo(() => getTranslations(entry), [entry]); + const onConfirm = useCallback(() => { + dispatch({ type: 'trustedAppDeletionDialogConfirmed' }); + }, [dispatch]); + const onCancel = useCallback(() => { + if (!isBusy) { + dispatch({ type: 'trustedAppDeletionDialogClosed' }); + } + }, [dispatch, isBusy]); + + if (useTrustedAppsSelector(isDeletionDialogOpen)) { + return ( + + + + {translations.title} + + + + +

{translations.mainMessage}

+

{translations.subMessage}

+
+
+ + + + {translations.cancelButton} + + + + {translations.confirmButton} + + +
+
+ ); + } else { + return <>; + } +}); + +TrustedAppDeletionDialog.displayName = 'TrustedAppDeletionDialog'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx index 0362f5c7a9de6..a457ecd0d088f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx @@ -3,40 +3,28 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { combineReducers, createStore } from 'redux'; import { render } from '@testing-library/react'; import React from 'react'; import { Provider } from 'react-redux'; -import { - MANAGEMENT_STORE_GLOBAL_NAMESPACE, - MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE, -} from '../../../common/constants'; -import { trustedAppsPageReducer } from '../store/reducer'; import { TrustedAppsList } from './trusted_apps_list'; import { + createSampleTrustedApp, createListFailedResourceState, createListLoadedResourceState, createListLoadingResourceState, createTrustedAppsListResourceStateChangedAction, createUserChangedUrlAction, + createGlobalNoMiddlewareStore, } from '../test_utils'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => 'mockId', })); -const createStoreSetup = () => { - return createStore( - combineReducers({ - [MANAGEMENT_STORE_GLOBAL_NAMESPACE]: combineReducers({ - [MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: trustedAppsPageReducer, - }), - }) - ); -}; +const now = 111111; -const renderList = (store: ReturnType) => { +const renderList = (store: ReturnType) => { const Wrapper: React.FC = ({ children }) => {children}; return render(, { wrapper: Wrapper }); @@ -44,11 +32,11 @@ const renderList = (store: ReturnType) => { describe('TrustedAppsList', () => { it('renders correctly initially', () => { - expect(renderList(createStoreSetup()).container).toMatchSnapshot(); + expect(renderList(createGlobalNoMiddlewareStore()).container).toMatchSnapshot(); }); it('renders correctly when loading data for the first time', () => { - const store = createStoreSetup(); + const store = createGlobalNoMiddlewareStore(); store.dispatch( createTrustedAppsListResourceStateChangedAction(createListLoadingResourceState()) @@ -58,7 +46,7 @@ describe('TrustedAppsList', () => { }); it('renders correctly when failed loading data for the first time', () => { - const store = createStoreSetup(); + const store = createGlobalNoMiddlewareStore(); store.dispatch( createTrustedAppsListResourceStateChangedAction( @@ -70,23 +58,23 @@ describe('TrustedAppsList', () => { }); it('renders correctly when loaded data', () => { - const store = createStoreSetup(); + const store = createGlobalNoMiddlewareStore(); store.dispatch( createTrustedAppsListResourceStateChangedAction( - createListLoadedResourceState({ index: 0, size: 20 }, 200) + createListLoadedResourceState({ index: 0, size: 20 }, 200, now) ) ); expect(renderList(store).container).toMatchSnapshot(); }); - it('renders correctly when new page and page sie set (not loading yet)', () => { - const store = createStoreSetup(); + it('renders correctly when new page and page size set (not loading yet)', () => { + const store = createGlobalNoMiddlewareStore(); store.dispatch( createTrustedAppsListResourceStateChangedAction( - createListLoadedResourceState({ index: 0, size: 20 }, 200) + createListLoadedResourceState({ index: 0, size: 20 }, 200, now) ) ); store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50')); @@ -95,11 +83,13 @@ describe('TrustedAppsList', () => { }); it('renders correctly when loading data for the second time', () => { - const store = createStoreSetup(); + const store = createGlobalNoMiddlewareStore(); store.dispatch( createTrustedAppsListResourceStateChangedAction( - createListLoadingResourceState(createListLoadedResourceState({ index: 0, size: 20 }, 200)) + createListLoadingResourceState( + createListLoadedResourceState({ index: 0, size: 20 }, 200, now) + ) ) ); @@ -107,17 +97,37 @@ describe('TrustedAppsList', () => { }); it('renders correctly when failed loading data for the second time', () => { - const store = createStoreSetup(); + const store = createGlobalNoMiddlewareStore(); store.dispatch( createTrustedAppsListResourceStateChangedAction( createListFailedResourceState( 'Intenal Server Error', - createListLoadedResourceState({ index: 0, size: 20 }, 200) + createListLoadedResourceState({ index: 0, size: 20 }, 200, now) ) ) ); expect(renderList(store).container).toMatchSnapshot(); }); + + it('triggers deletion dialog when delete action clicked', async () => { + const store = createGlobalNoMiddlewareStore(); + + store.dispatch( + createTrustedAppsListResourceStateChangedAction( + createListLoadedResourceState({ index: 0, size: 20 }, 200, now) + ) + ); + store.dispatch = jest.fn(); + + (await renderList(store).findAllByTestId('trustedAppDeleteAction'))[0].click(); + + expect(store.dispatch).toBeCalledWith({ + type: 'trustedAppDeletionDialogStarted', + payload: { + entry: createSampleTrustedApp(0), + }, + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx index ea834060d5223..c91512d477510 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx @@ -4,12 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Dispatch } from 'redux'; import React, { memo, useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; +import { EuiBasicTable, EuiBasicTableColumn, EuiTableActionsColumnType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Immutable } from '../../../../../common/endpoint/types'; +import { AppAction } from '../../../../common/store/actions'; import { TrustedApp } from '../../../../../common/endpoint/types/trusted_apps'; import { MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../../common/constants'; import { getTrustedAppsListPath } from '../../../common/routing'; @@ -28,7 +31,9 @@ import { useTrustedAppsSelector } from './hooks'; import { FormattedDate } from '../../../../common/components/formatted_date'; import { OS_TITLES } from './constants'; -const COLUMN_TITLES: Readonly<{ [K in keyof Omit]: string }> = { +const COLUMN_TITLES: Readonly< + { [K in keyof Omit | 'actions']: string } +> = { name: i18n.translate('xpack.securitySolution.trustedapps.list.columns.name', { defaultMessage: 'Name', }), @@ -41,9 +46,41 @@ const COLUMN_TITLES: Readonly<{ [K in keyof Omit]: created_by: i18n.translate('xpack.securitySolution.trustedapps.list.columns.createdBy', { defaultMessage: 'Created By', }), + actions: i18n.translate('xpack.securitySolution.trustedapps.list.columns.actions', { + defaultMessage: 'Actions', + }), }; -const getColumnDefinitions = (): Array>> => [ +type ActionsList = EuiTableActionsColumnType>['actions']; + +const getActionDefinitions = (dispatch: Dispatch>): ActionsList => [ + { + name: i18n.translate('xpack.securitySolution.trustedapps.list.actions.delete', { + defaultMessage: 'Delete', + }), + description: i18n.translate( + 'xpack.securitySolution.trustedapps.list.actions.delete.description', + { + defaultMessage: 'Delete this entry', + } + ), + 'data-test-subj': 'trustedAppDeleteAction', + isPrimary: true, + icon: 'trash', + color: 'danger', + type: 'icon', + onClick: (item: Immutable) => { + dispatch({ + type: 'trustedAppDeletionDialogStarted', + payload: { entry: item }, + }); + }, + }, +]; + +type ColumnsList = Array>>; + +const getColumnDefinitions = (dispatch: Dispatch>): ColumnsList => [ { field: 'name', name: COLUMN_TITLES.name, @@ -72,6 +109,10 @@ const getColumnDefinitions = (): Array field: 'created_by', name: COLUMN_TITLES.created_by, }, + { + name: COLUMN_TITLES.actions, + actions: getActionDefinitions(dispatch), + }, ]; export const TrustedAppsList = memo(() => { @@ -79,11 +120,12 @@ export const TrustedAppsList = memo(() => { const pageSize = useTrustedAppsSelector(getListCurrentPageSize); const totalItemCount = useTrustedAppsSelector(getListTotalItemsCount); const listItems = useTrustedAppsSelector(getListItems); + const dispatch = useDispatch(); const history = useHistory(); return ( getColumnDefinitions(dispatch), [dispatch])} items={useMemo(() => [...listItems], [listItems])} error={useTrustedAppsSelector(getListErrorMessage)} loading={useTrustedAppsSelector(isListLoading)} diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.test.tsx new file mode 100644 index 0000000000000..cc45abf493582 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.test.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Provider } from 'react-redux'; +import { render } from '@testing-library/react'; + +import { NotificationsStart } from 'kibana/public'; + +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public/context'; + +import { + createGlobalNoMiddlewareStore, + createSampleTrustedApp, + createServerApiError, +} from '../test_utils'; + +import { TrustedAppsNotifications } from './trusted_apps_notifications'; + +const mockNotifications = () => coreMock.createStart({ basePath: '/mock' }).notifications; + +const renderNotifications = ( + store: ReturnType, + notifications: NotificationsStart +) => { + const Wrapper: React.FC = ({ children }) => ( + + {children} + + ); + + return render(, { wrapper: Wrapper }); +}; + +describe('TrustedAppsNotifications', () => { + it('renders correctly initially', () => { + const notifications = mockNotifications(); + + renderNotifications(createGlobalNoMiddlewareStore(), notifications); + + expect(notifications.toasts.addSuccess).not.toBeCalled(); + expect(notifications.toasts.addDanger).not.toBeCalled(); + }); + + it('shows success notification when deletion successful', () => { + const store = createGlobalNoMiddlewareStore(); + const notifications = mockNotifications(); + + renderNotifications(store, notifications); + + store.dispatch({ + type: 'trustedAppDeletionDialogStarted', + payload: { entry: createSampleTrustedApp(3) }, + }); + store.dispatch({ + type: 'trustedAppDeletionSubmissionResourceStateChanged', + payload: { newState: { type: 'LoadedResourceState', data: null } }, + }); + store.dispatch({ + type: 'trustedAppDeletionDialogClosed', + }); + + expect(notifications.toasts.addSuccess).toBeCalledWith({ + text: '"trusted app 3" has been removed from the Trusted Applications list.', + title: 'Successfully removed', + }); + expect(notifications.toasts.addDanger).not.toBeCalled(); + }); + + it('shows error notification when deletion fails', () => { + const store = createGlobalNoMiddlewareStore(); + const notifications = mockNotifications(); + + renderNotifications(store, notifications); + + store.dispatch({ + type: 'trustedAppDeletionDialogStarted', + payload: { entry: createSampleTrustedApp(3) }, + }); + store.dispatch({ + type: 'trustedAppDeletionSubmissionResourceStateChanged', + payload: { + newState: { type: 'FailedResourceState', error: createServerApiError('Not Found') }, + }, + }); + + expect(notifications.toasts.addSuccess).not.toBeCalled(); + expect(notifications.toasts.addDanger).toBeCalledWith({ + text: + 'Unable to remove "trusted app 3" from the Trusted Applications list. Reason: Not Found', + title: 'Removal failure', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx new file mode 100644 index 0000000000000..9c0fe8eb6f0cb --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { memo } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { ServerApiError } from '../../../../common/types'; +import { Immutable, TrustedApp } from '../../../../../common/endpoint/types'; +import { getDeletionDialogEntry, getDeletionError, isDeletionSuccessful } from '../store/selectors'; + +import { useToasts } from '../../../../common/lib/kibana'; +import { useTrustedAppsSelector } from './hooks'; + +const getDeletionErrorMessage = (error: ServerApiError, entry: Immutable) => { + return { + title: i18n.translate('xpack.securitySolution.trustedapps.deletionError.title', { + defaultMessage: 'Removal failure', + }), + text: i18n.translate('xpack.securitySolution.trustedapps.deletionError.text', { + defaultMessage: + 'Unable to remove "{name}" from the Trusted Applications list. Reason: {message}', + values: { name: entry.name, message: error.message }, + }), + }; +}; + +const getDeletionSuccessMessage = (entry: Immutable) => { + return { + title: i18n.translate('xpack.securitySolution.trustedapps.deletionSuccess.title', { + defaultMessage: 'Successfully removed', + }), + text: i18n.translate('xpack.securitySolution.trustedapps.deletionSuccess.text', { + defaultMessage: '"{name}" has been removed from the Trusted Applications list.', + values: { name: entry?.name }, + }), + }; +}; + +export const TrustedAppsNotifications = memo(() => { + const deletionError = useTrustedAppsSelector(getDeletionError); + const deletionDialogEntry = useTrustedAppsSelector(getDeletionDialogEntry); + const deletionSuccessful = useTrustedAppsSelector(isDeletionSuccessful); + const toasts = useToasts(); + + if (deletionError && deletionDialogEntry) { + toasts.addDanger(getDeletionErrorMessage(deletionError, deletionDialogEntry)); + } + + if (deletionSuccessful && deletionDialogEntry) { + toasts.addSuccess(getDeletionSuccessMessage(deletionDialogEntry)); + } + + return <>; +}); + +TrustedAppsNotifications.displayName = 'TrustedAppsNotifications'; 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 218cef36ed50a..457f96dbff768 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 @@ -21,6 +21,15 @@ describe('TrustedAppsPage', () => { let coreStart: AppContextTestRender['coreStart']; let waitForAction: MiddlewareActionSpyHelper['waitForAction']; let render: () => ReturnType; + const originalScrollTo = window.scrollTo; + + beforeAll(() => { + window.scrollTo = () => {}; + }); + + afterAll(() => { + window.scrollTo = originalScrollTo; + }); beforeEach(() => { const mockedContext = createAppRootMockRenderer(); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index a0dae900eb30e..c1c23a3960962 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -9,6 +9,8 @@ import { EuiButton } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { AdministrationListPage } from '../../../components/administration_list_page'; import { TrustedAppsList } from './trusted_apps_list'; +import { TrustedAppDeletionDialog } from './trusted_app_deletion_dialog'; +import { TrustedAppsNotifications } from './trusted_apps_notifications'; import { CreateTrustedAppFlyout } from './components/create_trusted_app_flyout'; import { getTrustedAppsListPath } from '../../../common/routing'; import { useTrustedAppsSelector } from './hooks'; @@ -63,6 +65,8 @@ export const TrustedAppsPage = memo(() => { } actions={addButton} > + + {showAddFlout && ( = { [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: initialPolicyListState(), [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(), [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialEndpointListState, - [MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: initialTrustedAppsPageState, + [MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: initialTrustedAppsPageState(), }; /** diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts index ec4d1efb81b11..7cf3d467c10c2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts @@ -6,6 +6,7 @@ import { RequestHandler, RequestHandlerContext } from 'kibana/server'; import { + DeleteTrustedAppsRequestParams, GetTrustedAppsListRequest, GetTrustedListAppsResponse, PostTrustedAppCreateRequest, @@ -13,7 +14,6 @@ import { import { EndpointAppContext } from '../../types'; import { exceptionItemToTrustedAppItem, newTrustedAppItemToExceptionItem } from './utils'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; -import { DeleteTrustedAppsRequestParams } from './types'; import { ExceptionListClient } from '../../../../../lists/server'; const exceptionListClientFromContext = (context: RequestHandlerContext): ExceptionListClient => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts index 2368dcda09a38..98c9b79f32d6b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts @@ -18,6 +18,7 @@ import { TRUSTED_APPS_LIST_API, } from '../../../../common/endpoint/constants'; import { + DeleteTrustedAppsRequestParams, GetTrustedAppsListRequest, PostTrustedAppCreateRequest, } from '../../../../common/endpoint/types'; @@ -30,7 +31,6 @@ import { ExceptionListItemSchema, FoundExceptionListItemSchema, } from '../../../../../lists/common/schemas/response'; -import { DeleteTrustedAppsRequestParams } from './types'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; type RequestHandlerContextWithLists = ReturnType & { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/types.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/types.ts deleted file mode 100644 index 13c8bcfc20793..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/types.ts +++ /dev/null @@ -1,10 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { TypeOf } from '@kbn/config-schema'; -import { DeleteTrustedAppsRequestSchema } from '../../../../common/endpoint/schema/trusted_apps'; - -export type DeleteTrustedAppsRequestParams = TypeOf; From 8d4b5a599acaafd4fa0ee952ad6927e6738b3fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Wed, 23 Sep 2020 14:14:41 +0100 Subject: [PATCH 43/92] [APM] Round numeric values to 15 significant digits (#77899) * rounding numeric values to 5 decimals * fixing api tests * fixing api tests * addressing PR comment * fixing api tests * fixing tests --- .../tests/metrics_charts/metrics_charts.ts | 41 ++- .../observability_overview.ts | 6 +- .../basic/tests/services/top_services.ts | 30 +- .../traces/__snapshots__/top_traces.snap | 228 +++++++-------- .../basic/tests/traces/top_traces.ts | 2 +- .../__snapshots__/breakdown.snap | 110 ++++---- .../__snapshots__/error_rate.snap | 8 +- .../__snapshots__/top_transaction_groups.snap | 52 ++-- .../__snapshots__/transaction_charts.snap | 44 +-- .../tests/transaction_groups/error_rate.ts | 2 +- .../transaction_groups/transaction_charts.ts | 2 +- .../common/match_snapshot.ts | 16 +- .../__snapshots__/service_maps.snap | 260 +++++++++--------- .../trial/tests/service_maps/service_maps.ts | 4 +- 14 files changed, 409 insertions(+), 396 deletions(-) diff --git a/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts b/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts index ad3d1b0ccc4d9..cae562b3f5dc5 100644 --- a/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts +++ b/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts @@ -19,8 +19,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - // FLAKY: https://github.com/elastic/kibana/issues/77870 - describe.skip('when data is loaded', () => { + describe('when data is loaded', () => { before(() => esArchiver.load('metrics_8.0.0')); after(() => esArchiver.unload('metrics_8.0.0')); @@ -70,7 +69,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .toMatchInline(` Array [ 0.714, - 0.38770000000000004, + 0.3877, 0.75, 0.2543, ] @@ -100,8 +99,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` Array [ - 0.7220939209255549, - 0.7181735467963479, + 0.722093920925555, + 0.718173546796348, ] `); }); @@ -162,9 +161,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { .toMatchInline(` Array [ 0.203, - 0.17877777777777779, + 0.178777777777778, 0.01, - 0.009000000000000001, + 0.009, ] `); }); @@ -175,8 +174,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { Array [ 0.193, 0.193, - 0.009000000000000001, - 0.009000000000000001, + 0.009, + 0.009, ] `); }); @@ -204,8 +203,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` Array [ - 0.7079247035578369, - 0.7053959808411816, + 0.707924703557837, + 0.705395980841182, ] `); }); @@ -214,8 +213,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); expectSnapshot(yValues).toMatchInline(` Array [ - 0.7079247035578369, - 0.7079247035578369, + 0.707924703557837, + 0.707924703557837, ] `); }); @@ -244,7 +243,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` Array [ - 222501617.7777778, + 222501617.777778, 374341632, 1560281088, ] @@ -285,8 +284,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` Array [ - 138573397.33333334, - 147677639.1111111, + 138573397.333333, + 147677639.111111, ] `); }); @@ -324,7 +323,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` Array [ - 44.44444444444444, + 44.4444444444444, 45, ] `); @@ -423,16 +422,16 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` Array [ - 0.11452389642649889, - 0.11400237609041514, + 0.114523896426499, + 0.114002376090415, ] `); const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); expectSnapshot(yValues).toMatchInline(` Array [ - 0.11383724014063981, - 0.11383724014063981, + 0.11383724014064, + 0.11383724014064, ] `); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts b/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts index 5b04213401660..41564af55562a 100644 --- a/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts +++ b/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts @@ -64,15 +64,15 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, Object { "x": "2020-09-15T08:54:00.000Z", - "y": 1.8666666666666667, + "y": 1.86666666666667, }, Object { "x": "2020-09-15T08:55:00.000Z", - "y": 0.9666666666666667, + "y": 0.966666666666667, }, Object { "x": "2020-09-15T08:56:00.000Z", - "y": 1.9333333333333333, + "y": 1.93333333333333, }, Object { "x": "2020-09-15T08:57:00.000Z", diff --git a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts index 9eb9d80e26b6c..0e0d5cb21b71a 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts @@ -91,43 +91,43 @@ export default function ApiTest({ getService }: FtrProviderContext) { Array [ Object { "avgResponseTime": Object { - "value": 213583.7652495379, + "value": 213583.765249538, }, "transactionErrorRate": Object { "value": 0, }, "transactionsPerMinute": Object { - "value": 18.033333333333335, + "value": 18.0333333333333, }, }, Object { "avgResponseTime": Object { - "value": 600255.7079646018, + "value": 600255.707964602, }, "transactionErrorRate": Object { "value": 0, }, "transactionsPerMinute": Object { - "value": 7.533333333333333, + "value": 7.53333333333333, }, }, Object { "avgResponseTime": Object { - "value": 1818501.060810811, + "value": 1818501.06081081, }, "transactionErrorRate": Object { - "value": 0.02027027027027027, + "value": 0.0202702702702703, }, "transactionsPerMinute": Object { - "value": 4.933333333333334, + "value": 4.93333333333333, }, }, Object { "avgResponseTime": Object { - "value": 290900.5714285714, + "value": 290900.571428571, }, "transactionErrorRate": Object { - "value": 0.013605442176870748, + "value": 0.0136054421768707, }, "transactionsPerMinute": Object { "value": 4.9, @@ -135,10 +135,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, Object { "avgResponseTime": Object { - "value": 1123903.7027027027, + "value": 1123903.7027027, }, "transactionErrorRate": Object { - "value": 0.009009009009009009, + "value": 0.00900900900900901, }, "transactionsPerMinute": Object { "value": 3.7, @@ -146,10 +146,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, Object { "avgResponseTime": Object { - "value": 80364.62962962964, + "value": 80364.6296296296, }, "transactionErrorRate": Object { - "value": 0.18518518518518517, + "value": 0.185185185185185, }, "transactionsPerMinute": Object { "value": 3.6, @@ -157,10 +157,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, Object { "avgResponseTime": Object { - "value": 1365102.9411764706, + "value": 1365102.94117647, }, "transactionsPerMinute": Object { - "value": 2.2666666666666666, + "value": 2.26666666666667, }, }, ] diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap b/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap index cd5773d18d6b7..157bbccd109be 100644 --- a/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap +++ b/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap @@ -12,11 +12,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "POST /api/orders", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 3347, - "impact": 0.003559081182448518, + "impact": 0.00355908118244852, "key": Object { "service.name": "opbeans-python", "transaction.name": "GET opbeans.views.stats", @@ -24,7 +24,7 @@ Array [ "serviceName": "opbeans-python", "transactionName": "GET opbeans.views.stats", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 4479, @@ -36,11 +36,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "GET /api/customers/:id", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 7287, - "impact": 0.009904230439845424, + "impact": 0.00990423043984542, "key": Object { "service.name": "opbeans-node", "transaction.name": "GET /api/products/top", @@ -48,11 +48,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "GET /api/products/top", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 8023, - "impact": 0.011089517204678958, + "impact": 0.011089517204679, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::OrdersController#show", @@ -60,11 +60,11 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Api::OrdersController#show", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 8282, - "impact": 0.011506622193934236, + "impact": 0.0115066221939342, "key": Object { "service.name": "opbeans-node", "transaction.name": "GET /api/orders/:id", @@ -72,11 +72,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "GET /api/orders/:id", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 12116, - "impact": 0.017681064390091532, + "impact": 0.0176810643900915, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::ProductsController#top", @@ -84,11 +84,11 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Api::ProductsController#top", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 6451, - "impact": 0.018946873353622995, + "impact": 0.018946873353623, "key": Object { "service.name": "opbeans-node", "transaction.name": "GET /api/products", @@ -96,11 +96,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "GET /api/products", "transactionType": "request", - "transactionsPerMinute": 0.06666666666666667, + "transactionsPerMinute": 0.0666666666666667, }, Object { "averageResponseTime": 13360, - "impact": 0.019684456693696034, + "impact": 0.019684456693696, "key": Object { "service.name": "opbeans-java", "transaction.name": "APIRestController#customers", @@ -108,11 +108,11 @@ Array [ "serviceName": "opbeans-java", "transactionName": "APIRestController#customers", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 7903, - "impact": 0.023623602653998786, + "impact": 0.0236236026539988, "key": Object { "service.name": "opbeans-java", "transaction.name": "APIRestController#product", @@ -120,11 +120,11 @@ Array [ "serviceName": "opbeans-java", "transactionName": "APIRestController#product", "transactionType": "request", - "transactionsPerMinute": 0.06666666666666667, + "transactionsPerMinute": 0.0666666666666667, }, Object { "averageResponseTime": 17913, - "impact": 0.027016808107129565, + "impact": 0.0270168081071296, "key": Object { "service.name": "opbeans-node", "transaction.name": "GET /api/stats", @@ -132,11 +132,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "GET /api/stats", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { - "averageResponseTime": 6065.666666666667, - "impact": 0.02747417419573381, + "averageResponseTime": 6065.66666666667, + "impact": 0.0274741741957338, "key": Object { "service.name": "opbeans-java", "transaction.name": "APIRestController#topProducts", @@ -148,7 +148,7 @@ Array [ }, Object { "averageResponseTime": 2340.875, - "impact": 0.02832770950193187, + "impact": 0.0283277095019319, "key": Object { "service.name": "opbeans-java", "transaction.name": "ResourceHttpRequestHandler", @@ -156,11 +156,11 @@ Array [ "serviceName": "opbeans-java", "transactionName": "ResourceHttpRequestHandler", "transactionType": "request", - "transactionsPerMinute": 0.26666666666666666, + "transactionsPerMinute": 0.266666666666667, }, Object { - "averageResponseTime": 7340.666666666667, - "impact": 0.03363412239612548, + "averageResponseTime": 7340.66666666667, + "impact": 0.0336341223961255, "key": Object { "service.name": "opbeans-java", "transaction.name": "APIRestController#customerWhoBought", @@ -172,7 +172,7 @@ Array [ }, Object { "averageResponseTime": 7689, - "impact": 0.03531703634891222, + "impact": 0.0353170363489122, "key": Object { "service.name": "opbeans-node", "transaction.name": "GET /api/types", @@ -184,7 +184,7 @@ Array [ }, Object { "averageResponseTime": 11598, - "impact": 0.035524783621552876, + "impact": 0.0355247836215529, "key": Object { "service.name": "opbeans-node", "transaction.name": "GET /api/products/:id/customers", @@ -192,11 +192,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "GET /api/products/:id/customers", "transactionType": "request", - "transactionsPerMinute": 0.06666666666666667, + "transactionsPerMinute": 0.0666666666666667, }, Object { "averageResponseTime": 12077.5, - "impact": 0.03706919939257919, + "impact": 0.0370691993925792, "key": Object { "service.name": "opbeans-java", "transaction.name": "APIRestController#order", @@ -204,11 +204,11 @@ Array [ "serviceName": "opbeans-java", "transactionName": "APIRestController#order", "transactionType": "request", - "transactionsPerMinute": 0.06666666666666667, + "transactionsPerMinute": 0.0666666666666667, }, Object { "averageResponseTime": 6296.5, - "impact": 0.03872956712973051, + "impact": 0.0387295671297305, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::TypesController#index", @@ -216,11 +216,11 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Api::TypesController#index", "transactionType": "request", - "transactionsPerMinute": 0.13333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { "averageResponseTime": 28181, - "impact": 0.04355284683173653, + "impact": 0.0435528468317365, "key": Object { "service.name": "opbeans-python", "transaction.name": "GET opbeans.views.customer", @@ -228,11 +228,11 @@ Array [ "serviceName": "opbeans-python", "transactionName": "GET opbeans.views.customer", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 7439, - "impact": 0.046089296090721335, + "impact": 0.0460892960907213, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/customers/:id", @@ -240,10 +240,10 @@ Array [ "serviceName": "opbeans-go", "transactionName": "GET /api/customers/:id", "transactionType": "request", - "transactionsPerMinute": 0.13333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { - "averageResponseTime": 10471.333333333334, + "averageResponseTime": 10471.3333333333, "impact": 0.0487594121995447, "key": Object { "service.name": "opbeans-node", @@ -264,11 +264,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "GET /api/customers", "transactionType": "request", - "transactionsPerMinute": 0.06666666666666667, + "transactionsPerMinute": 0.0666666666666667, }, Object { "averageResponseTime": 11732.25, - "impact": 0.07374545045551247, + "impact": 0.0737454504555125, "key": Object { "service.name": "opbeans-java", "transaction.name": "APIRestController#customer", @@ -276,7 +276,7 @@ Array [ "serviceName": "opbeans-java", "transactionName": "APIRestController#customer", "transactionType": "request", - "transactionsPerMinute": 0.13333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { "averageResponseTime": 47646, @@ -288,11 +288,11 @@ Array [ "serviceName": "opbeans-python", "transactionName": "GET opbeans.views.customers", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 13160.75, - "impact": 0.08294752732271193, + "impact": 0.0829475273227119, "key": Object { "service.name": "opbeans-python", "transaction.name": "GET opbeans.views.products", @@ -300,11 +300,11 @@ Array [ "serviceName": "opbeans-python", "transactionName": "GET opbeans.views.products", "transactionType": "request", - "transactionsPerMinute": 0.13333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { - "averageResponseTime": 4131.461538461538, - "impact": 0.08466426059895181, + "averageResponseTime": 4131.46153846154, + "impact": 0.0846642605989518, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/types/:id", @@ -312,11 +312,11 @@ Array [ "serviceName": "opbeans-go", "transactionName": "GET /api/types/:id", "transactionType": "request", - "transactionsPerMinute": 0.43333333333333335, + "transactionsPerMinute": 0.433333333333333, }, Object { "averageResponseTime": 13869.25, - "impact": 0.08751152554491062, + "impact": 0.0875115255449106, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::StatsController#index", @@ -324,11 +324,11 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Api::StatsController#index", "transactionType": "request", - "transactionsPerMinute": 0.13333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { - "averageResponseTime": 20643.333333333332, - "impact": 0.09790372050886552, + "averageResponseTime": 20643.3333333333, + "impact": 0.0979037205088655, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::ProductsController#show", @@ -340,7 +340,7 @@ Array [ }, Object { "averageResponseTime": 15596.5, - "impact": 0.09863808296099064, + "impact": 0.0986380829609906, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::TypesController#show", @@ -348,11 +348,11 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Api::TypesController#show", "transactionType": "request", - "transactionsPerMinute": 0.13333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { "averageResponseTime": 20989, - "impact": 0.09957375090986059, + "impact": 0.0995737509098606, "key": Object { "service.name": "opbeans-python", "transaction.name": "GET opbeans.views.orders", @@ -364,7 +364,7 @@ Array [ }, Object { "averageResponseTime": 74419, - "impact": 0.11801655529963453, + "impact": 0.118016555299635, "key": Object { "service.name": "opbeans-python", "transaction.name": "GET opbeans.views.product_type", @@ -372,11 +372,11 @@ Array [ "serviceName": "opbeans-python", "transactionName": "GET opbeans.views.product_type", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { - "averageResponseTime": 10678.42857142857, - "impact": 0.11854800181104089, + "averageResponseTime": 10678.4285714286, + "impact": 0.118548001811041, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/orders/:id", @@ -384,11 +384,11 @@ Array [ "serviceName": "opbeans-go", "transactionName": "GET /api/orders/:id", "transactionType": "request", - "transactionsPerMinute": 0.23333333333333334, + "transactionsPerMinute": 0.233333333333333, }, Object { - "averageResponseTime": 27078.666666666668, - "impact": 0.12899495187011034, + "averageResponseTime": 27078.6666666667, + "impact": 0.12899495187011, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::OrdersController#index", @@ -399,8 +399,8 @@ Array [ "transactionsPerMinute": 0.1, }, Object { - "averageResponseTime": 11827.42857142857, - "impact": 0.13150080269358994, + "averageResponseTime": 11827.4285714286, + "impact": 0.13150080269359, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/customers", @@ -408,11 +408,11 @@ Array [ "serviceName": "opbeans-go", "transactionName": "GET /api/customers", "transactionType": "request", - "transactionsPerMinute": 0.23333333333333334, + "transactionsPerMinute": 0.233333333333333, }, Object { "averageResponseTime": 21770.75, - "impact": 0.13841121778584634, + "impact": 0.138411217785846, "key": Object { "service.name": "opbeans-python", "transaction.name": "GET opbeans.views.product", @@ -420,11 +420,11 @@ Array [ "serviceName": "opbeans-python", "transactionName": "GET opbeans.views.product", "transactionType": "request", - "transactionsPerMinute": 0.13333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { "averageResponseTime": 10252, - "impact": 0.1467613697908217, + "impact": 0.146761369790822, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/types", @@ -436,7 +436,7 @@ Array [ }, Object { "averageResponseTime": 100570, - "impact": 0.16013127566262603, + "impact": 0.160131275662626, "key": Object { "service.name": "opbeans-python", "transaction.name": "GET opbeans.views.top_products", @@ -444,11 +444,11 @@ Array [ "serviceName": "opbeans-python", "transactionName": "GET opbeans.views.top_products", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 15505, - "impact": 0.1979283957314345, + "impact": 0.197928395731435, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::CustomersController#index", @@ -456,11 +456,11 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Api::CustomersController#index", "transactionType": "request", - "transactionsPerMinute": 0.26666666666666666, + "transactionsPerMinute": 0.266666666666667, }, Object { "averageResponseTime": 22856.5, - "impact": 0.21902360134631826, + "impact": 0.219023601346318, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/products", @@ -472,7 +472,7 @@ Array [ }, Object { "averageResponseTime": 17250.125, - "impact": 0.2204118040518706, + "impact": 0.220411804051871, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::ProductsController#index", @@ -480,11 +480,11 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Api::ProductsController#index", "transactionType": "request", - "transactionsPerMinute": 0.26666666666666666, + "transactionsPerMinute": 0.266666666666667, }, Object { - "averageResponseTime": 20089.555555555555, - "impact": 0.2893468583571687, + "averageResponseTime": 20089.5555555556, + "impact": 0.289346858357169, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::CustomersController#show", @@ -495,8 +495,8 @@ Array [ "transactionsPerMinute": 0.3, }, Object { - "averageResponseTime": 26487.85714285714, - "impact": 0.29676939463314395, + "averageResponseTime": 26487.8571428571, + "impact": 0.296769394633144, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/stats", @@ -504,11 +504,11 @@ Array [ "serviceName": "opbeans-go", "transactionName": "GET /api/stats", "transactionType": "request", - "transactionsPerMinute": 0.23333333333333334, + "transactionsPerMinute": 0.233333333333333, }, Object { - "averageResponseTime": 14957.538461538461, - "impact": 0.31131653504991197, + "averageResponseTime": 14957.5384615385, + "impact": 0.311316535049912, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/products/:id/customers", @@ -516,7 +516,7 @@ Array [ "serviceName": "opbeans-go", "transactionName": "GET /api/products/:id/customers", "transactionType": "request", - "transactionsPerMinute": 0.43333333333333335, + "transactionsPerMinute": 0.433333333333333, }, Object { "averageResponseTime": 30178.5, @@ -528,11 +528,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "GET /api", "transactionType": "request", - "transactionsPerMinute": 0.4666666666666667, + "transactionsPerMinute": 0.466666666666667, }, Object { "averageResponseTime": 32625.875, - "impact": 0.8388432258236366, + "impact": 0.838843225823637, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/products/:id", @@ -540,11 +540,11 @@ Array [ "serviceName": "opbeans-go", "transactionName": "GET /api/products/:id", "transactionType": "request", - "transactionsPerMinute": 0.5333333333333333, + "transactionsPerMinute": 0.533333333333333, }, Object { - "averageResponseTime": 121200.83333333333, - "impact": 1.1692918352841768, + "averageResponseTime": 121200.833333333, + "impact": 1.16929183528418, "key": Object { "service.name": "opbeans-python", "transaction.name": "GET opbeans.views.product_customers", @@ -555,8 +555,8 @@ Array [ "transactionsPerMinute": 0.2, }, Object { - "averageResponseTime": 38025.86666666667, - "impact": 3.6724805948748136, + "averageResponseTime": 38025.8666666667, + "impact": 3.67248059487481, "key": Object { "service.name": "opbeans-python", "transaction.name": "opbeans.tasks.sync_orders", @@ -579,8 +579,8 @@ Array [ "transactionsPerMinute": 0.3, }, Object { - "averageResponseTime": 691636.3636363636, - "impact": 12.25042667907868, + "averageResponseTime": 691636.363636364, + "impact": 12.2504266790787, "key": Object { "service.name": "opbeans-rum", "transaction.name": "/customers", @@ -588,11 +588,11 @@ Array [ "serviceName": "opbeans-rum", "transactionName": "/customers", "transactionType": "page-load", - "transactionsPerMinute": 0.36666666666666664, + "transactionsPerMinute": 0.366666666666667, }, Object { "averageResponseTime": 1590910.5, - "impact": 20.494746747861388, + "impact": 20.4947467478614, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/orders", @@ -600,11 +600,11 @@ Array [ "serviceName": "opbeans-go", "transactionName": "GET /api/orders", "transactionType": "request", - "transactionsPerMinute": 0.26666666666666666, + "transactionsPerMinute": 0.266666666666667, }, Object { - "averageResponseTime": 303589.16279069765, - "impact": 21.02144244954455, + "averageResponseTime": 303589.162790698, + "impact": 21.0214424495446, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Rack", @@ -612,11 +612,11 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Rack", "transactionType": "request", - "transactionsPerMinute": 1.4333333333333333, + "transactionsPerMinute": 1.43333333333333, }, Object { "averageResponseTime": 1180200, - "impact": 28.507858596190804, + "impact": 28.5078585961908, "key": Object { "service.name": "opbeans-rum", "transaction.name": "/products", @@ -627,8 +627,8 @@ Array [ "transactionsPerMinute": 0.5, }, Object { - "averageResponseTime": 1073178.5714285714, - "impact": 48.390399898683754, + "averageResponseTime": 1073178.57142857, + "impact": 48.3903998986838, "key": Object { "service.name": "opbeans-rum", "transaction.name": "/dashboard", @@ -636,11 +636,11 @@ Array [ "serviceName": "opbeans-rum", "transactionName": "/dashboard", "transactionType": "page-load", - "transactionsPerMinute": 0.9333333333333333, + "transactionsPerMinute": 0.933333333333333, }, Object { - "averageResponseTime": 2676214.285714286, - "impact": 60.33667329750868, + "averageResponseTime": 2676214.28571429, + "impact": 60.3366732975087, "key": Object { "service.name": "opbeans-rum", "transaction.name": "/orders", @@ -648,11 +648,11 @@ Array [ "serviceName": "opbeans-rum", "transactionName": "/orders", "transactionType": "page-load", - "transactionsPerMinute": 0.4666666666666667, + "transactionsPerMinute": 0.466666666666667, }, Object { - "averageResponseTime": 928922.4347826086, - "impact": 68.81313564424958, + "averageResponseTime": 928922.434782609, + "impact": 68.8131356442496, "key": Object { "service.name": "opbeans-node", "transaction.name": "Process completed order", @@ -660,11 +660,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "Process completed order", "transactionType": "Worker", - "transactionsPerMinute": 1.5333333333333334, + "transactionsPerMinute": 1.53333333333333, }, Object { - "averageResponseTime": 1012219.0930232558, - "impact": 70.09342088866295, + "averageResponseTime": 1012219.09302326, + "impact": 70.0934208886629, "key": Object { "service.name": "opbeans-node", "transaction.name": "Process payment", @@ -672,11 +672,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "Process payment", "transactionType": "Worker", - "transactionsPerMinute": 1.4333333333333333, + "transactionsPerMinute": 1.43333333333333, }, Object { - "averageResponseTime": 126010.60833333334, - "impact": 73.05405786950051, + "averageResponseTime": 126010.608333333, + "impact": 73.0540578695005, "key": Object { "service.name": "opbeans-python", "transaction.name": "opbeans.tasks.update_stats", @@ -687,8 +687,8 @@ Array [ "transactionsPerMinute": 12, }, Object { - "averageResponseTime": 1041680.2444444444, - "impact": 75.48871418577934, + "averageResponseTime": 1041680.24444444, + "impact": 75.4887141857793, "key": Object { "service.name": "opbeans-node", "transaction.name": "Update shipping status", diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts b/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts index 3429301e4a326..b6fccf8f5b581 100644 --- a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts +++ b/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts @@ -73,7 +73,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { "serviceName": "opbeans-node", "transactionName": "POST /api/orders", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, } `); diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap index 563bad8779e96..87938f6f1f122 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap @@ -36,7 +36,7 @@ Object { }, Object { "x": 1600160190000, - "y": 0.4827586206896552, + "y": 0.482758620689655, }, Object { "x": 1600160220000, @@ -52,7 +52,7 @@ Object { }, Object { "x": 1600160310000, - "y": 0.17142857142857143, + "y": 0.171428571428571, }, Object { "x": 1600160340000, @@ -68,15 +68,15 @@ Object { }, Object { "x": 1600160430000, - "y": 0.41964285714285715, + "y": 0.419642857142857, }, Object { "x": 1600160460000, - "y": 0.7222222222222222, + "y": 0.722222222222222, }, Object { "x": 1600160490000, - "y": 0.8333333333333334, + "y": 0.833333333333333, }, Object { "x": 1600160520000, @@ -88,7 +88,7 @@ Object { }, Object { "x": 1600160580000, - "y": 0.11044776119402985, + "y": 0.11044776119403, }, Object { "x": 1600160610000, @@ -100,15 +100,15 @@ Object { }, Object { "x": 1600160670000, - "y": 0.15028901734104047, + "y": 0.15028901734104, }, Object { "x": 1600160700000, - "y": 0.38095238095238093, + "y": 0.380952380952381, }, Object { "x": 1600160730000, - "y": 0.06761565836298933, + "y": 0.0676156583629893, }, Object { "x": 1600160760000, @@ -116,7 +116,7 @@ Object { }, Object { "x": 1600160790000, - "y": 0.26373626373626374, + "y": 0.263736263736264, }, Object { "x": 1600160820000, @@ -124,7 +124,7 @@ Object { }, Object { "x": 1600160850000, - "y": 0.5294117647058824, + "y": 0.529411764705882, }, Object { "x": 1600160880000, @@ -132,11 +132,11 @@ Object { }, Object { "x": 1600160910000, - "y": 0.012096774193548387, + "y": 0.0120967741935484, }, Object { "x": 1600160940000, - "y": 0.26126126126126126, + "y": 0.261261261261261, }, Object { "x": 1600160970000, @@ -148,11 +148,11 @@ Object { }, Object { "x": 1600161030000, - "y": 0.16071428571428573, + "y": 0.160714285714286, }, Object { "x": 1600161060000, - "y": 0.040268456375838924, + "y": 0.0402684563758389, }, Object { "x": 1600161090000, @@ -164,11 +164,11 @@ Object { }, Object { "x": 1600161150000, - "y": 0.07894736842105263, + "y": 0.0789473684210526, }, Object { "x": 1600161180000, - "y": 0.4074074074074074, + "y": 0.407407407407407, }, Object { "x": 1600161210000, @@ -180,11 +180,11 @@ Object { }, Object { "x": 1600161270000, - "y": 0.6666666666666666, + "y": 0.666666666666667, }, Object { "x": 1600161300000, - "y": 0.8214285714285714, + "y": 0.821428571428571, }, Object { "x": 1600161330000, @@ -196,11 +196,11 @@ Object { }, Object { "x": 1600161390000, - "y": 0.17333333333333334, + "y": 0.173333333333333, }, Object { "x": 1600161420000, - "y": 0.14285714285714285, + "y": 0.142857142857143, }, Object { "x": 1600161450000, @@ -212,7 +212,7 @@ Object { }, Object { "x": 1600161510000, - "y": 0.42105263157894735, + "y": 0.421052631578947, }, Object { "x": 1600161540000, @@ -232,7 +232,7 @@ Object { }, Object { "x": 1600161660000, - "y": 0.018518518518518517, + "y": 0.0185185185185185, }, Object { "x": 1600161690000, @@ -244,11 +244,11 @@ Object { }, Object { "x": 1600161750000, - "y": 0.36764705882352944, + "y": 0.367647058823529, }, Object { "x": 1600161780000, - "y": 0.10526315789473684, + "y": 0.105263157894737, }, ], "hideLegend": false, @@ -289,7 +289,7 @@ Object { }, Object { "x": 1600160190000, - "y": 0.41379310344827586, + "y": 0.413793103448276, }, Object { "x": 1600160220000, @@ -305,7 +305,7 @@ Object { }, Object { "x": 1600160310000, - "y": 0.6285714285714286, + "y": 0.628571428571429, }, Object { "x": 1600160340000, @@ -341,7 +341,7 @@ Object { }, Object { "x": 1600160580000, - "y": 0.8895522388059701, + "y": 0.88955223880597, }, Object { "x": 1600160610000, @@ -353,7 +353,7 @@ Object { }, Object { "x": 1600160670000, - "y": 0.7052023121387283, + "y": 0.705202312138728, }, Object { "x": 1600160700000, @@ -361,7 +361,7 @@ Object { }, Object { "x": 1600160730000, - "y": 0.8718861209964412, + "y": 0.871886120996441, }, Object { "x": 1600160760000, @@ -369,7 +369,7 @@ Object { }, Object { "x": 1600160790000, - "y": 0.6703296703296703, + "y": 0.67032967032967, }, Object { "x": 1600160820000, @@ -385,11 +385,11 @@ Object { }, Object { "x": 1600160910000, - "y": 0.9879032258064516, + "y": 0.987903225806452, }, Object { "x": 1600160940000, - "y": 0.7387387387387387, + "y": 0.738738738738739, }, Object { "x": 1600160970000, @@ -401,7 +401,7 @@ Object { }, Object { "x": 1600161030000, - "y": 0.7946428571428571, + "y": 0.794642857142857, }, Object { "x": 1600161060000, @@ -417,7 +417,7 @@ Object { }, Object { "x": 1600161150000, - "y": 0.9210526315789473, + "y": 0.921052631578947, }, Object { "x": 1600161180000, @@ -449,11 +449,11 @@ Object { }, Object { "x": 1600161390000, - "y": 0.7466666666666667, + "y": 0.746666666666667, }, Object { "x": 1600161420000, - "y": 0.8571428571428571, + "y": 0.857142857142857, }, Object { "x": 1600161450000, @@ -465,7 +465,7 @@ Object { }, Object { "x": 1600161510000, - "y": 0.5789473684210527, + "y": 0.578947368421053, }, Object { "x": 1600161540000, @@ -485,7 +485,7 @@ Object { }, Object { "x": 1600161660000, - "y": 0.9814814814814815, + "y": 0.981481481481482, }, Object { "x": 1600161690000, @@ -497,11 +497,11 @@ Object { }, Object { "x": 1600161750000, - "y": 0.5588235294117647, + "y": 0.558823529411765, }, Object { "x": 1600161780000, - "y": 0.8947368421052632, + "y": 0.894736842105263, }, ], "hideLegend": false, @@ -542,7 +542,7 @@ Object { }, Object { "x": 1600160190000, - "y": 0.10344827586206896, + "y": 0.103448275862069, }, Object { "x": 1600160220000, @@ -574,15 +574,15 @@ Object { }, Object { "x": 1600160430000, - "y": 0.14285714285714285, + "y": 0.142857142857143, }, Object { "x": 1600160460000, - "y": 0.2777777777777778, + "y": 0.277777777777778, }, Object { "x": 1600160490000, - "y": 0.16666666666666666, + "y": 0.166666666666667, }, Object { "x": 1600160520000, @@ -606,15 +606,15 @@ Object { }, Object { "x": 1600160670000, - "y": 0.14450867052023122, + "y": 0.144508670520231, }, Object { "x": 1600160700000, - "y": 0.6190476190476191, + "y": 0.619047619047619, }, Object { "x": 1600160730000, - "y": 0.060498220640569395, + "y": 0.0604982206405694, }, Object { "x": 1600160760000, @@ -622,7 +622,7 @@ Object { }, Object { "x": 1600160790000, - "y": 0.06593406593406594, + "y": 0.0659340659340659, }, Object { "x": 1600160820000, @@ -630,7 +630,7 @@ Object { }, Object { "x": 1600160850000, - "y": 0.47058823529411764, + "y": 0.470588235294118, }, Object { "x": 1600160880000, @@ -654,7 +654,7 @@ Object { }, Object { "x": 1600161030000, - "y": 0.044642857142857144, + "y": 0.0446428571428571, }, Object { "x": 1600161060000, @@ -674,7 +674,7 @@ Object { }, Object { "x": 1600161180000, - "y": 0.5925925925925926, + "y": 0.592592592592593, }, Object { "x": 1600161210000, @@ -686,11 +686,11 @@ Object { }, Object { "x": 1600161270000, - "y": 0.3333333333333333, + "y": 0.333333333333333, }, Object { "x": 1600161300000, - "y": 0.17857142857142858, + "y": 0.178571428571429, }, Object { "x": 1600161330000, @@ -750,7 +750,7 @@ Object { }, Object { "x": 1600161750000, - "y": 0.07352941176470588, + "y": 0.0735294117647059, }, Object { "x": 1600161780000, diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/error_rate.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/error_rate.snap index f9ab0ed8ff8cf..ab228385aaf56 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/error_rate.snap +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/error_rate.snap @@ -12,7 +12,7 @@ Array [ }, Object { "x": 1600160040000, - "y": 0.14285714285714285, + "y": 0.142857142857143, }, Object { "x": 1600160070000, @@ -44,11 +44,11 @@ Array [ }, Object { "x": 1600160280000, - "y": 0.16666666666666666, + "y": 0.166666666666667, }, Object { "x": 1600160310000, - "y": 0.3333333333333333, + "y": 0.333333333333333, }, Object { "x": 1600160340000, @@ -76,7 +76,7 @@ Array [ }, Object { "x": 1600160520000, - "y": 0.16666666666666666, + "y": 0.166666666666667, }, Object { "x": 1600160550000, diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap index e37b2283f009a..93f22e67e1a02 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap @@ -10,41 +10,41 @@ Array [ "serviceName": "opbeans-node", "transactionName": "POST /api/orders", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 4479, - "impact": 0.1825278966745733, + "impact": 0.182527896674573, "key": "GET /api/customers/:id", "p95": 4448, "serviceName": "opbeans-node", "transactionName": "GET /api/customers/:id", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 2754.5, - "impact": 0.23878275411766442, + "impact": 0.238782754117664, "key": "GET /*", "p95": 2832, "serviceName": "opbeans-node", "transactionName": "GET /*", "transactionType": "request", - "transactionsPerMinute": 0.06666666666666667, + "transactionsPerMinute": 0.0666666666666667, }, Object { "averageResponseTime": 10841, - "impact": 1.122093248707094, + "impact": 1.12209324870709, "key": "GET /api/orders/:id", "p95": 13376, "serviceName": "opbeans-node", "transactionName": "GET /api/orders/:id", "transactionType": "request", - "transactionsPerMinute": 0.06666666666666667, + "transactionsPerMinute": 0.0666666666666667, }, Object { - "averageResponseTime": 10551.333333333334, - "impact": 1.6667276549425354, + "averageResponseTime": 10551.3333333333, + "impact": 1.66672765494254, "key": "GET /api/products/top", "p95": 19552, "serviceName": "opbeans-node", @@ -54,37 +54,37 @@ Array [ }, Object { "averageResponseTime": 15988, - "impact": 1.6843141249393074, + "impact": 1.68431412493931, "key": "GET /api/products/:id", "p95": 16000, "serviceName": "opbeans-node", "transactionName": "GET /api/products/:id", "transactionType": "request", - "transactionsPerMinute": 0.06666666666666667, + "transactionsPerMinute": 0.0666666666666667, }, Object { "averageResponseTime": 9499, - "impact": 2.013104650965918, + "impact": 2.01310465096592, "key": "GET /api/types", "p95": 14944, "serviceName": "opbeans-node", "transactionName": "GET /api/types", "transactionType": "request", - "transactionsPerMinute": 0.13333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { "averageResponseTime": 12228, - "impact": 2.6092969071297842, + "impact": 2.60929690712978, "key": "GET /api/products/:id/customers", "p95": 17760, "serviceName": "opbeans-node", "transactionName": "GET /api/products/:id/customers", "transactionType": "request", - "transactionsPerMinute": 0.13333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { - "averageResponseTime": 22555.666666666668, - "impact": 3.633626859892089, + "averageResponseTime": 22555.6666666667, + "impact": 3.63362685989209, "key": "GET /api/customers", "p95": 25984, "serviceName": "opbeans-node", @@ -94,17 +94,17 @@ Array [ }, Object { "averageResponseTime": 13852.6, - "impact": 3.7207945807456553, + "impact": 3.72079458074566, "key": "GET /api/types/:id", "p95": 21984, "serviceName": "opbeans-node", "transactionName": "GET /api/types/:id", "transactionType": "request", - "transactionsPerMinute": 0.16666666666666666, + "transactionsPerMinute": 0.166666666666667, }, Object { "averageResponseTime": 12228.5, - "impact": 3.9451586141206243, + "impact": 3.94515861412062, "key": "GET /api/orders", "p95": 16736, "serviceName": "opbeans-node", @@ -113,18 +113,18 @@ Array [ "transactionsPerMinute": 0.2, }, Object { - "averageResponseTime": 12491.42857142857, + "averageResponseTime": 12491.4285714286, "impact": 4.71355627370009, "key": "GET /api/products", "p95": 30448, "serviceName": "opbeans-node", "transactionName": "GET /api/products", "transactionType": "request", - "transactionsPerMinute": 0.23333333333333334, + "transactionsPerMinute": 0.233333333333333, }, Object { - "averageResponseTime": 23683.333333333332, - "impact": 11.579379700079686, + "averageResponseTime": 23683.3333333333, + "impact": 11.5793797000797, "key": "GET /api/stats", "p95": 36288, "serviceName": "opbeans-node", @@ -133,14 +133,14 @@ Array [ "transactionsPerMinute": 0.3, }, Object { - "averageResponseTime": 42606.74418604651, + "averageResponseTime": 42606.7441860465, "impact": 100, "key": "GET /api", "p95": 131008, "serviceName": "opbeans-node", "transactionName": "GET /api", "transactionType": "request", - "transactionsPerMinute": 1.4333333333333333, + "transactionsPerMinute": 1.43333333333333, }, ] `; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap index aaeac9edf01b8..9ed103b445575 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap @@ -3,7 +3,7 @@ exports[`Transaction charts when data is loaded returns the correct data 4`] = ` Object { "apmTimeseries": Object { - "overallAvgDuration": 600255.7079646018, + "overallAvgDuration": 600255.707964602, "responseTimes": Object { "avg": Array [ Object { @@ -32,11 +32,11 @@ Object { }, Object { "x": 1600160160000, - "y": 467003.6666666667, + "y": 467003.666666667, }, Object { "x": 1600160190000, - "y": 863809.6666666666, + "y": 863809.666666667, }, Object { "x": 1600160220000, @@ -64,7 +64,7 @@ Object { }, Object { "x": 1600160400000, - "y": 368087.9090909091, + "y": 368087.909090909, }, Object { "x": 1600160430000, @@ -92,11 +92,11 @@ Object { }, Object { "x": 1600160610000, - "y": 882789.6666666666, + "y": 882789.666666667, }, Object { "x": 1600160640000, - "y": 238075.9090909091, + "y": 238075.909090909, }, Object { "x": 1600160670000, @@ -112,11 +112,11 @@ Object { }, Object { "x": 1600160760000, - "y": 282337.1666666667, + "y": 282337.166666667, }, Object { "x": 1600160790000, - "y": 987012.3333333334, + "y": 987012.333333333, }, Object { "x": 1600160820000, @@ -136,7 +136,7 @@ Object { }, Object { "x": 1600160940000, - "y": 1313632.6666666667, + "y": 1313632.66666667, }, Object { "x": 1600160970000, @@ -144,11 +144,11 @@ Object { }, Object { "x": 1600161000000, - "y": 611899.1428571428, + "y": 611899.142857143, }, Object { "x": 1600161030000, - "y": 273321.85714285716, + "y": 273321.857142857, }, Object { "x": 1600161060000, @@ -156,7 +156,7 @@ Object { }, Object { "x": 1600161090000, - "y": 1446104.6666666667, + "y": 1446104.66666667, }, Object { "x": 1600161120000, @@ -172,11 +172,11 @@ Object { }, Object { "x": 1600161210000, - "y": 1054428.6666666667, + "y": 1054428.66666667, }, Object { "x": 1600161240000, - "y": 816781.3333333334, + "y": 816781.333333333, }, Object { "x": 1600161270000, @@ -192,7 +192,7 @@ Object { }, Object { "x": 1600161360000, - "y": 714202.3333333334, + "y": 714202.333333333, }, Object { "x": 1600161390000, @@ -204,7 +204,7 @@ Object { }, Object { "x": 1600161450000, - "y": 836182.3333333334, + "y": 836182.333333333, }, Object { "x": 1600161480000, @@ -212,11 +212,11 @@ Object { }, Object { "x": 1600161510000, - "y": 615193.3333333334, + "y": 615193.333333333, }, Object { "x": 1600161540000, - "y": 946298.6666666666, + "y": 946298.666666667, }, Object { "x": 1600161570000, @@ -240,7 +240,7 @@ Object { }, Object { "x": 1600161720000, - "y": 450557.77777777775, + "y": 450557.777777778, }, Object { "x": 1600161750000, @@ -746,7 +746,7 @@ Object { }, "tpmBuckets": Array [ Object { - "avg": 2.8333333333333335, + "avg": 2.83333333333333, "dataPoints": Array [ Object { "x": 1600159980000, @@ -996,7 +996,7 @@ Object { "key": "HTTP 2xx", }, Object { - "avg": 0.23333333333333334, + "avg": 0.233333333333333, "dataPoints": Array [ Object { "x": 1600159980000, @@ -1246,7 +1246,7 @@ Object { "key": "HTTP 4xx", }, Object { - "avg": 4.466666666666667, + "avg": 4.46666666666667, "dataPoints": Array [ Object { "x": 1600159980000, diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts index 27a2eac3131f5..17ada95ca4958 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts @@ -80,7 +80,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('has the correct calculation for average', () => { - expectSnapshot(errorRateResponse.average).toMatchInline(`0.14086309523809523`); + expectSnapshot(errorRateResponse.average).toMatchInline(`0.140863095238095`); }); it('has the correct error rate', () => { diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts index 8dd52ef241c59..ef874695e6046 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts @@ -62,7 +62,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the correct data', () => { expectSnapshot(response.body.apmTimeseries.overallAvgDuration).toMatchInline( - `600255.7079646018` + `600255.707964602` ); expectSnapshot(response.body.apmTimeseries.responseTimes.avg.length).toMatchInline(`61`); expectSnapshot(response.body.apmTimeseries.tpmBuckets.length).toMatchInline(`3`); diff --git a/x-pack/test/apm_api_integration/common/match_snapshot.ts b/x-pack/test/apm_api_integration/common/match_snapshot.ts index 4ac812a0ee168..d260a19b60df4 100644 --- a/x-pack/test/apm_api_integration/common/match_snapshot.ts +++ b/x-pack/test/apm_api_integration/common/match_snapshot.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotState, toMatchSnapshot, toMatchInlineSnapshot } from 'jest-snapshot'; +import { + SnapshotState, + toMatchSnapshot, + toMatchInlineSnapshot, + addSerializer, +} from 'jest-snapshot'; import path from 'path'; import expect from '@kbn/expect'; // @ts-expect-error @@ -62,6 +67,15 @@ export function registerMochaHooksForSnapshots() { { snapshotState: ISnapshotState; testsInFile: Test[] } > = {}; + addSerializer({ + serialize: (num: number) => { + return String(parseFloat(num.toPrecision(15))); + }, + test: (value: any) => { + return typeof value === 'number'; + }, + }); + registered = true; beforeEach(function () { diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap b/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap index bf42c08438156..8a3929f1e9ba6 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap @@ -75,8 +75,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -103,8 +103,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -117,7 +117,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -137,8 +137,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -151,7 +151,7 @@ Array [ "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -171,8 +171,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -185,7 +185,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -204,7 +204,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -232,7 +232,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -246,8 +246,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -266,7 +266,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -280,7 +280,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -300,7 +300,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -314,8 +314,8 @@ Array [ "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -333,7 +333,7 @@ Array [ "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -361,7 +361,7 @@ Array [ "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -375,8 +375,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -395,7 +395,7 @@ Array [ "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -409,7 +409,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -428,7 +428,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -455,7 +455,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -482,7 +482,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -510,7 +510,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -524,8 +524,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -544,7 +544,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -558,7 +558,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -578,7 +578,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -592,7 +592,7 @@ Array [ "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -611,8 +611,8 @@ Array [ "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -638,8 +638,8 @@ Array [ "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -652,8 +652,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -672,8 +672,8 @@ Array [ "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -686,7 +686,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -705,8 +705,8 @@ Array [ "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -719,7 +719,7 @@ Array [ "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -738,8 +738,8 @@ Array [ "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -752,7 +752,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -784,8 +784,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -816,7 +816,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -848,7 +848,7 @@ Array [ "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -880,7 +880,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -912,8 +912,8 @@ Array [ "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -942,7 +942,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -957,7 +957,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -972,7 +972,7 @@ Array [ "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -987,8 +987,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1002,8 +1002,8 @@ Array [ "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -1061,8 +1061,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1089,8 +1089,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1103,7 +1103,7 @@ Object { "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1123,8 +1123,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1137,7 +1137,7 @@ Object { "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -1157,8 +1157,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1171,7 +1171,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1190,7 +1190,7 @@ Object { "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1218,7 +1218,7 @@ Object { "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1232,8 +1232,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1252,7 +1252,7 @@ Object { "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1266,7 +1266,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1286,7 +1286,7 @@ Object { "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1300,8 +1300,8 @@ Object { "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -1319,7 +1319,7 @@ Object { "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -1347,7 +1347,7 @@ Object { "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -1361,8 +1361,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1381,7 +1381,7 @@ Object { "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -1395,7 +1395,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1414,7 +1414,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1441,7 +1441,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1468,7 +1468,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1496,7 +1496,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1510,8 +1510,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1530,7 +1530,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1544,7 +1544,7 @@ Object { "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1564,7 +1564,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1578,7 +1578,7 @@ Object { "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -1597,8 +1597,8 @@ Object { "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -1624,8 +1624,8 @@ Object { "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -1638,8 +1638,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1658,8 +1658,8 @@ Object { "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -1672,7 +1672,7 @@ Object { "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1691,8 +1691,8 @@ Object { "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -1705,7 +1705,7 @@ Object { "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -1724,8 +1724,8 @@ Object { "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -1738,7 +1738,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1770,8 +1770,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1802,7 +1802,7 @@ Object { "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1834,7 +1834,7 @@ Object { "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -1866,7 +1866,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1898,8 +1898,8 @@ Object { "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -1928,7 +1928,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1943,7 +1943,7 @@ Object { "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1958,7 +1958,7 @@ Object { "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -1973,8 +1973,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1988,8 +1988,8 @@ Object { "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts index 2e4a859f08cca..a8632d7a27c3c 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts @@ -177,7 +177,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -192,7 +192,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", From 0cf3bf2731a222313639d52bc839d7d92c9b2011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Wed, 23 Sep 2020 14:44:40 +0100 Subject: [PATCH 44/92] [Observability] Collect UI telemetry (#78258) * adding telemetry to obs pages * adding telemetry to obs pages * increasing delay --- x-pack/plugins/observability/kibana.json | 16 ++------- .../public/application/application.test.tsx | 9 +++-- .../public/application/index.tsx | 36 ++++++++++++------- .../observability/public/data_handler.ts | 19 +++++----- .../public/pages/landing/index.tsx | 4 +++ .../public/pages/overview/index.tsx | 8 +++-- x-pack/plugins/observability/public/plugin.ts | 6 ++-- .../typings/fetch_overview_data/index.ts | 6 +++- .../public/typings/section/index.ts | 4 +-- .../plugins/observability/typings/common.ts | 2 +- 10 files changed, 64 insertions(+), 46 deletions(-) diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index 834982009b9d0..2b7c067f66bae 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -2,19 +2,9 @@ "id": "observability", "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": [ - "xpack", - "observability" - ], - "optionalPlugins": [ - "licensing", - "home" - ], + "configPath": ["xpack", "observability"], + "optionalPlugins": ["licensing", "home", "usageCollection"], "ui": true, "server": true, - "requiredBundles": [ - "data", - "kibanaReact", - "kibanaUtils" - ] + "requiredBundles": ["data", "kibanaReact", "kibanaUtils"] } diff --git a/x-pack/plugins/observability/public/application/application.test.tsx b/x-pack/plugins/observability/public/application/application.test.tsx index 19995ed233e8d..1304936860b77 100644 --- a/x-pack/plugins/observability/public/application/application.test.tsx +++ b/x-pack/plugins/observability/public/application/application.test.tsx @@ -3,15 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import { createMemoryHistory } from 'history'; import React from 'react'; import { Observable } from 'rxjs'; import { AppMountParameters, CoreStart } from 'src/core/public'; +import { ObservabilityPluginSetupDeps } from '../plugin'; import { renderApp } from './'; describe('renderApp', () => { - it('renders', () => { + it('renders', async () => { + const plugins = ({ + usageCollection: { reportUiStats: () => {} }, + } as unknown) as ObservabilityPluginSetupDeps; const core = ({ application: { currentAppId$: new Observable(), navigateToUrl: () => {} }, chrome: { docTitle: { change: () => {} }, setBreadcrumbs: () => {} }, @@ -24,7 +27,7 @@ describe('renderApp', () => { } as unknown) as AppMountParameters; expect(() => { - const unmount = renderApp(core, params); + const unmount = renderApp(core, plugins, params); unmount(); }).not.toThrowError(); }); diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 6cb7e8d9cf8fd..a6f1f7c5b7cf9 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -8,12 +8,16 @@ import React, { useEffect } from 'react'; import ReactDOM from 'react-dom'; import { Route, Router, Switch } from 'react-router-dom'; import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; -import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public'; +import { + KibanaContextProvider, + RedirectAppLinks, +} from '../../../../../src/plugins/kibana_react/public'; import { EuiThemeProvider } from '../../../xpack_legacy/common'; import { PluginContext } from '../context/plugin_context'; import { usePluginContext } from '../hooks/use_plugin_context'; import { useRouteParams } from '../hooks/use_route_params'; import { Breadcrumbs, routes } from '../routes'; +import { ObservabilityPluginSetupDeps } from '../plugin'; const observabilityLabelBreadcrumb = { text: i18n.translate('xpack.observability.observability.breadcrumb.', { @@ -51,22 +55,28 @@ function App() { ); } -export const renderApp = (core: CoreStart, { element, history }: AppMountParameters) => { +export const renderApp = ( + core: CoreStart, + plugins: ObservabilityPluginSetupDeps, + { element, history }: AppMountParameters +) => { const i18nCore = core.i18n; const isDarkMode = core.uiSettings.get('theme:darkMode'); ReactDOM.render( - - - - - - - - - - - , + + + + + + + + + + + + + , element ); return () => { diff --git a/x-pack/plugins/observability/public/data_handler.ts b/x-pack/plugins/observability/public/data_handler.ts index b0bdcf17b9066..cae21fd9fed52 100644 --- a/x-pack/plugins/observability/public/data_handler.ts +++ b/x-pack/plugins/observability/public/data_handler.ts @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DataHandler } from './typings/fetch_overview_data'; -import { ObservabilityApp } from '../typings/common'; +import { DataHandler, ObservabilityFetchDataPlugins } from './typings/fetch_overview_data'; -const dataHandlers: Partial> = {}; +const dataHandlers: Partial> = {}; -export function registerDataHandler({ +export function registerDataHandler({ appName, fetchData, hasData, @@ -17,19 +16,23 @@ export function registerDataHandler({ dataHandlers[appName] = { fetchData, hasData }; } -export function unregisterDataHandler({ appName }: { appName: T }) { +export function unregisterDataHandler({ + appName, +}: { + appName: T; +}) { delete dataHandlers[appName]; } -export function getDataHandler(appName: T) { +export function getDataHandler(appName: T) { const dataHandler = dataHandlers[appName]; if (dataHandler) { return dataHandler as DataHandler; } } -export async function fetchHasData(): Promise> { - const apps: ObservabilityApp[] = ['apm', 'uptime', 'infra_logs', 'infra_metrics']; +export async function fetchHasData(): Promise> { + const apps: ObservabilityFetchDataPlugins[] = ['apm', 'uptime', 'infra_logs', 'infra_metrics']; const promises = apps.map(async (app) => getDataHandler(app)?.hasData() || false); diff --git a/x-pack/plugins/observability/public/pages/landing/index.tsx b/x-pack/plugins/observability/public/pages/landing/index.tsx index 4d8bd4bf2c789..66a52091ae04d 100644 --- a/x-pack/plugins/observability/public/pages/landing/index.tsx +++ b/x-pack/plugins/observability/public/pages/landing/index.tsx @@ -21,6 +21,7 @@ import styled, { ThemeContext } from 'styled-components'; import { IngestManagerPanel } from '../../components/app/ingest_manager_panel'; import { WithHeaderLayout } from '../../components/app/layout/with_header'; import { usePluginContext } from '../../hooks/use_plugin_context'; +import { useTrackPageview } from '../../hooks/use_track_metric'; import { appsSection } from '../home/section'; const EuiCardWithoutPadding = styled(EuiCard)` @@ -28,6 +29,9 @@ const EuiCardWithoutPadding = styled(EuiCard)` `; export function LandingPage() { + useTrackPageview({ app: 'observability', path: 'landing' }); + useTrackPageview({ app: 'observability', path: 'landing', delay: 15000 }); + const { core } = usePluginContext(); const theme = useContext(ThemeContext); diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index 10bbdaaae34a8..3d10e4abcbb42 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -8,6 +8,7 @@ import React, { useContext } from 'react'; import { ThemeContext } from 'styled-components'; import { EmptySection } from '../../components/app/empty_section'; import { WithHeaderLayout } from '../../components/app/layout/with_header'; +import { NewsFeed } from '../../components/app/news_feed'; import { Resources } from '../../components/app/resources'; import { AlertsSection } from '../../components/app/section/alerts'; import { APMSection } from '../../components/app/section/apm'; @@ -15,18 +16,18 @@ import { LogsSection } from '../../components/app/section/logs'; import { MetricsSection } from '../../components/app/section/metrics'; import { UptimeSection } from '../../components/app/section/uptime'; import { DatePicker, TimePickerTime } from '../../components/shared/data_picker'; -import { NewsFeed } from '../../components/app/news_feed'; import { fetchHasData } from '../../data_handler'; import { FETCH_STATUS, useFetcher } from '../../hooks/use_fetcher'; import { UI_SETTINGS, useKibanaUISettings } from '../../hooks/use_kibana_ui_settings'; import { usePluginContext } from '../../hooks/use_plugin_context'; +import { useTrackPageview } from '../../hooks/use_track_metric'; import { RouteParams } from '../../routes'; +import { getNewsFeed } from '../../services/get_news_feed'; import { getObservabilityAlerts } from '../../services/get_observability_alerts'; import { getAbsoluteTime } from '../../utils/date'; import { getBucketSize } from '../../utils/get_bucket_size'; import { getEmptySections } from './empty_section'; import { LoadingObservability } from './loading_observability'; -import { getNewsFeed } from '../../services/get_news_feed'; interface Props { routeParams: RouteParams<'/overview'>; @@ -41,6 +42,9 @@ function calculatetBucketSize({ start, end }: { start?: number; end?: number }) export function OverviewPage({ routeParams }: Props) { const { core } = usePluginContext(); + useTrackPageview({ app: 'observability', path: 'overview' }); + useTrackPageview({ app: 'observability', path: 'overview', delay: 15000 }); + const { data: alerts = [], status: alertStatus } = useFetcher(() => { return getObservabilityAlerts({ core }); }, [core]); diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 0a82f37d10a7b..be8abb4dcac78 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -23,7 +23,7 @@ export interface ObservabilityPluginSetup { dashboard: { register: typeof registerDataHandler }; } -interface SetupPlugins { +export interface ObservabilityPluginSetupDeps { home?: HomePublicPluginSetup; } @@ -34,7 +34,7 @@ export class Plugin implements PluginClass = ( export type HasData = () => Promise; -export interface DataHandler { +export type ObservabilityFetchDataPlugins = Exclude; + +export interface DataHandler< + T extends ObservabilityFetchDataPlugins = ObservabilityFetchDataPlugins +> { fetchData: FetchData; hasData: HasData; } diff --git a/x-pack/plugins/observability/public/typings/section/index.ts b/x-pack/plugins/observability/public/typings/section/index.ts index f336b6b981687..d70d8ac8617bb 100644 --- a/x-pack/plugins/observability/public/typings/section/index.ts +++ b/x-pack/plugins/observability/public/typings/section/index.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ObservabilityApp } from '../../../typings/common'; +import { ObservabilityFetchDataPlugins } from '../fetch_overview_data'; export interface ISection { - id: ObservabilityApp | 'alert'; + id: ObservabilityFetchDataPlugins | 'alert'; title: string; icon: string; description: string; diff --git a/x-pack/plugins/observability/typings/common.ts b/x-pack/plugins/observability/typings/common.ts index 19afac0c0d2b8..c1b01c847f164 100644 --- a/x-pack/plugins/observability/typings/common.ts +++ b/x-pack/plugins/observability/typings/common.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export type ObservabilityApp = 'infra_metrics' | 'infra_logs' | 'apm' | 'uptime'; +export type ObservabilityApp = 'infra_metrics' | 'infra_logs' | 'apm' | 'uptime' | 'observability'; export type PromiseReturnType = Func extends (...args: any[]) => Promise ? Value From 35a6a230cd8b67e9c194f39771bca81109757a6b Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Wed, 23 Sep 2020 09:57:41 -0400 Subject: [PATCH 45/92] [Resolver] Refactoring panel view (#77928) * Moved `descriptiveName` from the 'common' event model into the panel view. It is now a component. Each type of event has its own translation string. Translation placeholders have more specific names. * Reorganized 'breadcrumb' components. * Use safer types many places * Add `useLinkProps` hook. It takes `PanelViewAndParameters` and returns `onClick` and `href`. Remove a bunch of copy-pasted code that did the same. * Add new common event methods to safely expose fields that were being read directly (`processPID`, `userName`, `userDomain`, `parentPID`, `md5HashForProcess`, `argsForProcess` * Removed 'primaryEventCategory' from the event model. * Removed the 'aggregate' total count concept from the panel * The mock data access layer calle no_ancestors_two_children now has related events. This will allow the click through to test all panels and it will allow the resolver test plugin to view all panels. * The `mockEndpointEvent` factory can now return events of any type instead of just process events. * Several mocks that were using unsafe casting now return the correct types. The unsafe casting was fine for testing but it made refactoring difficult because typescript couldn't find issues. * The mock helper function `withRelatedEventsOnOrigin` now takes the related events to add to the origin instead of an array describing events to be created. * The data state's `tree` field was optional but the initial state incorrectly set it to an invalid object. Now code checks for the presence of a tree object. * Added a selector called `eventByID` which is used to get the event shown in the event detail panel. This will be replaced with an API call in the near future. * Added a selector called `relatedEventCountByType` which finds the count of related events for a type from the `byCategory` structure returned from the API. We should consider changing this as it requires metaprogramming as it is. * Created a new middleware 'fetcher' to fetch related events. This is a stop-gap implementation that we expect to replace before release. * Removed the action called `appDetectedNewIdFromQueryParams`. Use `appReceivedNewExternal...` instead. * Added the first simulator test for a graph node. It checks that the origin node has 'Analyzed Event' in the label. * Added a new panel test that navigates to the nodeEvents panel view and verifies the items in the list. * Added a new panel component called 'Breadcrumbs'. * Fixed an issue where the CubeForProcess component was using `0 0 100% 100%` in the `viewBox` attribute. * The logic that calculates the 'entries' to show when viewing the details of an event was moved into a separate function and unit tested. It is called `deepObjectEntries`. * The code that shows the name of an event is now a component called `DescriptiveName`. It has an enzyme test. Each event type has its own `i18n` string which includes more descriptive placeholders. I'm not sure, but I think this will make it possible for translators to provide better contextual formatting around the values. * Refactored most panel views. They have loading components and breadcrumb components. Links are moved to their own components, allowing them to call `useLinkProps`. * Introduced a hook called `useLinkProps` which combines the `relativeHref` selector with the `useNavigateOrReplace` hook. * Removed the hook called `useRelatedEventDetailNavigation`. Use `useLinkProps` instead. * Move various styled-components into `styles` modules. * The graph node label wasn't translating 'Analyzed Event'. It now does so using a `select` expression in the ICU message. * Renamed a method on the common event model from `getAncestryAsArray` to `ancestry` for consistency. It no longer takes `undefined` for the event it operates on. * Some translations were removed due to code de-duping. --- .../common/endpoint/models/event.test.ts | 48 +- .../common/endpoint/models/event.ts | 180 +++---- .../common/endpoint/types/index.ts | 6 +- .../mocks/no_ancestors_two_children.ts | 9 +- ..._children_in_index_called_awesome_index.ts | 2 +- ..._children_with_related_events_on_origin.ts | 2 +- .../public/resolver/index.ts | 4 +- .../public/resolver/mocks/endpoint_event.ts | 35 +- .../public/resolver/mocks/resolver_tree.ts | 222 ++++---- .../resolver/models/process_event.test.ts | 26 +- .../public/resolver/models/process_event.ts | 94 ++-- .../public/resolver/models/resolver_tree.ts | 14 +- .../public/resolver/store/actions.ts | 35 +- .../public/resolver/store/data/action.ts | 9 - .../resolver/store/data/reducer.test.ts | 148 +----- .../public/resolver/store/data/reducer.ts | 16 +- .../resolver/store/data/selectors.test.ts | 4 +- .../public/resolver/store/data/selectors.ts | 424 +++++----------- .../store/data/visible_entities.test.ts | 6 +- .../public/resolver/store/methods.ts | 6 +- .../public/resolver/store/middleware/index.ts | 28 +- .../middleware/related_events_fetcher.ts | 49 ++ .../public/resolver/store/reducer.ts | 30 +- .../public/resolver/store/selectors.ts | 52 +- .../public/resolver/store/ui/selectors.ts | 10 +- .../public/resolver/types.ts | 6 +- .../public/resolver/view/edge_line.tsx | 8 +- .../public/resolver/view/graph_controls.tsx | 8 +- .../public/resolver/view/limit_warnings.tsx | 36 +- .../public/resolver/view/node.test.tsx | 42 ++ .../public/resolver/view/panel.test.tsx | 41 +- .../resolver/view/panels/breadcrumbs.tsx | 40 ++ .../resolver/view/panels/cube_for_process.tsx | 2 +- .../view/panels/deep_object_entries.test.ts | 36 ++ .../view/panels/deep_object_entries.ts | 44 ++ .../view/panels/descriptive_name.test.tsx | 50 ++ .../resolver/view/panels/descriptive_name.tsx | 114 +++++ .../resolver/view/panels/event_detail.tsx | 472 +++++++++--------- .../public/resolver/view/panels/index.tsx | 8 +- .../{node_details.tsx => node_detail.tsx} | 76 ++- .../resolver/view/panels/node_events.tsx | 218 ++++---- .../view/panels/node_events_of_type.tsx | 426 +++++++--------- .../public/resolver/view/panels/node_list.tsx | 175 +++---- .../view/panels/panel_content_error.tsx | 19 +- .../view/panels/panel_content_utilities.tsx | 62 +-- .../resolver/view/panels/panel_loading.tsx | 16 +- .../public/resolver/view/panels/styles.tsx | 50 ++ .../resolver/view/process_event_dot.tsx | 23 +- .../public/resolver/view/styles.tsx | 25 +- .../public/resolver/view/use_camera.test.tsx | 12 +- .../public/resolver/view/use_link_props.ts | 32 ++ ...se_related_event_by_category_navigation.ts | 2 +- .../use_related_event_detail_navigation.ts | 40 -- .../resolver/utils/ancestry_query_handler.ts | 7 +- .../routes/resolver/utils/children_helper.ts | 19 +- .../translations/translations/ja-JP.json | 5 - .../translations/translations/zh-CN.json | 5 - .../applications/resolver_test/index.tsx | 6 +- 58 files changed, 1668 insertions(+), 1916 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/view/node.test.tsx create mode 100644 x-pack/plugins/security_solution/public/resolver/view/panels/breadcrumbs.tsx create mode 100644 x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.test.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.ts create mode 100644 x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.test.tsx create mode 100644 x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.tsx rename x-pack/plugins/security_solution/public/resolver/view/panels/{node_details.tsx => node_detail.tsx} (74%) create mode 100644 x-pack/plugins/security_solution/public/resolver/view/use_link_props.ts delete mode 100644 x-pack/plugins/security_solution/public/resolver/view/use_related_event_detail_navigation.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts index 2b0aa1601ab37..fed32293e00a1 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { EndpointDocGenerator } from '../generate_data'; -import { descriptiveName, isProcessRunning } from './event'; -import { ResolverEvent, SafeResolverEvent } from '../types'; +import { isProcessRunning } from './event'; +import { SafeResolverEvent } from '../types'; describe('Generated documents', () => { let generator: EndpointDocGenerator; @@ -13,50 +13,6 @@ describe('Generated documents', () => { generator = new EndpointDocGenerator('seed'); }); - describe('Event descriptive names', () => { - it('returns the right name for a registry event', () => { - const extensions = { registry: { key: `HKLM/Windows/Software/abc` } }; - const event = generator.generateEvent({ eventCategory: 'registry', extensions }); - // casting to ResolverEvent here because the `descriptiveName` function is used by the frontend is still relies - // on the unsafe ResolverEvent type. Once it's switched over to the safe version we can remove this cast. - expect(descriptiveName(event as ResolverEvent)).toEqual({ - subject: `HKLM/Windows/Software/abc`, - }); - }); - - it('returns the right name for a network event', () => { - const randomIP = `${generator.randomIP()}`; - const extensions = { network: { direction: 'outbound', forwarded_ip: randomIP } }; - const event = generator.generateEvent({ eventCategory: 'network', extensions }); - // casting to ResolverEvent here because the `descriptiveName` function is used by the frontend is still relies - // on the unsafe ResolverEvent type. Once it's switched over to the safe version we can remove this cast. - expect(descriptiveName(event as ResolverEvent)).toEqual({ - subject: `${randomIP}`, - descriptor: 'outbound', - }); - }); - - it('returns the right name for a file event', () => { - const extensions = { file: { path: 'C:\\My Documents\\business\\January\\processName' } }; - const event = generator.generateEvent({ eventCategory: 'file', extensions }); - // casting to ResolverEvent here because the `descriptiveName` function is used by the frontend is still relies - // on the unsafe ResolverEvent type. Once it's switched over to the safe version we can remove this cast. - expect(descriptiveName(event as ResolverEvent)).toEqual({ - subject: 'C:\\My Documents\\business\\January\\processName', - }); - }); - - it('returns the right name for a dns event', () => { - const extensions = { dns: { question: { name: `${generator.randomIP()}` } } }; - const event = generator.generateEvent({ eventCategory: 'dns', extensions }); - // casting to ResolverEvent here because the `descriptiveName` function is used by the frontend is still relies - // on the unsafe ResolverEvent type. Once it's switched over to the safe version we can remove this cast. - expect(descriptiveName(event as ResolverEvent)).toEqual({ - subject: extensions.dns.question.name, - }); - }); - }); - describe('Process running events', () => { it('is a running event when event.type is a string', () => { const event: SafeResolverEvent = generator.generateEvent({ diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.ts index 9634659b1a5dd..00eb48bb62a5f 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/event.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/event.ts @@ -104,11 +104,14 @@ export function timestampAsDateSafeVersion(event: TimestampFields): Date | undef } } -export function eventTimestamp(event: ResolverEvent): string | undefined | number { - return event['@timestamp']; +export function eventTimestamp(event: SafeResolverEvent): string | undefined | number { + return firstNonNullValue(event['@timestamp']); } -export function eventName(event: ResolverEvent): string { +/** + * Find the name of the related process. + */ +export function processName(event: ResolverEvent): string { if (isLegacyEvent(event)) { return event.endgame.process_name ? event.endgame.process_name : ''; } else { @@ -116,6 +119,58 @@ export function eventName(event: ResolverEvent): string { } } +/** + * First non-null value in the `user.name` field. + */ +export function userName(event: SafeResolverEvent): string | undefined { + if (isLegacyEventSafeVersion(event)) { + return undefined; + } else { + return firstNonNullValue(event.user?.name); + } +} + +/** + * Returns the process event's parent PID + */ +export function parentPID(event: SafeResolverEvent): number | undefined { + return firstNonNullValue( + isLegacyEventSafeVersion(event) ? event.endgame.ppid : event.process?.parent?.pid + ); +} + +/** + * First non-null value for the `process.hash.md5` field. + */ +export function md5HashForProcess(event: SafeResolverEvent): string | undefined { + return firstNonNullValue(isLegacyEventSafeVersion(event) ? undefined : event.process?.hash?.md5); +} + +/** + * First non-null value for the `event.process.args` field. + */ +export function argsForProcess(event: SafeResolverEvent): string | undefined { + if (isLegacyEventSafeVersion(event)) { + // There is not currently a key for this on Legacy event types + return undefined; + } + return firstNonNullValue(event.process?.args); +} + +/** + * First non-null value in the `user.name` field. + */ +export function userDomain(event: SafeResolverEvent): string | undefined { + if (isLegacyEventSafeVersion(event)) { + return undefined; + } else { + return firstNonNullValue(event.user?.domain); + } +} + +/** + * Find the name of the related process. + */ export function processNameSafeVersion(event: SafeResolverEvent): string | undefined { if (isLegacyEventSafeVersion(event)) { return firstNonNullValue(event.endgame.process_name); @@ -124,11 +179,10 @@ export function processNameSafeVersion(event: SafeResolverEvent): string | undef } } -export function eventId(event: ResolverEvent): number | undefined | string { - if (isLegacyEvent(event)) { - return event.endgame.serial_event_id; - } - return event.event.id; +export function eventID(event: SafeResolverEvent): number | undefined | string { + return firstNonNullValue( + isLegacyEventSafeVersion(event) ? event.endgame.serial_event_id : event.event?.id + ); } /** @@ -275,18 +329,14 @@ export function ancestryArray(event: AncestryArrayFields): string[] | undefined /** * Minimum fields needed from the `SafeResolverEvent` type for the function below to operate correctly. */ -type GetAncestryArrayFields = AncestryArrayFields & ParentEntityIDFields; +type AncestryFields = AncestryArrayFields & ParentEntityIDFields; /** * Returns an array of strings representing the ancestry for a process. * * @param event an ES document */ -export function getAncestryAsArray(event: GetAncestryArrayFields | undefined): string[] { - if (!event) { - return []; - } - +export function ancestry(event: AncestryFields): string[] { const ancestors = ancestryArray(event); if (ancestors) { return ancestors; @@ -300,35 +350,13 @@ export function getAncestryAsArray(event: GetAncestryArrayFields | undefined): s return []; } -/** - * @param event The event to get the category for - */ -export function primaryEventCategory(event: ResolverEvent): string | undefined { - if (isLegacyEvent(event)) { - const legacyFullType = event.endgame.event_type_full; - if (legacyFullType) { - return legacyFullType; - } - } else { - const eventCategories = event.event.category; - const category = typeof eventCategories === 'string' ? eventCategories : eventCategories[0]; - - return category; - } -} - /** * @param event The event to get the full ECS category for */ -export function allEventCategories(event: ResolverEvent): string | string[] | undefined { - if (isLegacyEvent(event)) { - const legacyFullType = event.endgame.event_type_full; - if (legacyFullType) { - return legacyFullType; - } - } else { - return event.event.category; - } +export function eventCategory(event: SafeResolverEvent): string[] { + return values( + isLegacyEventSafeVersion(event) ? event.endgame.event_type_full : event.event?.category + ); } /** @@ -336,71 +364,19 @@ export function allEventCategories(event: ResolverEvent): string | string[] | un * see: https://www.elastic.co/guide/en/ecs/current/ecs-event.html * @param event The ResolverEvent to get the ecs type for */ -export function ecsEventType(event: ResolverEvent): Array { - if (isLegacyEvent(event)) { - return [event.endgame.event_subtype_full]; - } - return typeof event.event.type === 'string' ? [event.event.type] : event.event.type; +export function eventType(event: SafeResolverEvent): string[] { + return values( + isLegacyEventSafeVersion(event) ? event.endgame.event_subtype_full : event.event?.type + ); } /** - * #Descriptive Names For Related Events: - * - * The following section provides facilities for deriving **Descriptive Names** for ECS-compliant event data. - * There are drawbacks to trying to do this: It *will* require ongoing maintenance. It presents temptations to overarticulate. - * On balance, however, it seems that the benefit of giving the user some form of information they can recognize & scan outweighs the drawbacks. + * event.kind as an array. */ -type DeepPartial = T extends object ? { [K in keyof T]?: DeepPartial } : T; -/** - * Based on the ECS category of the event, attempt to provide a more descriptive name - * (e.g. the `event.registry.key` for `registry` or the `dns.question.name` for `dns`, etc.). - * This function returns the data in the form of `{subject, descriptor}` where `subject` will - * tend to be the more distinctive term (e.g. 137.213.212.7 for a network event) and the - * `descriptor` can be used to present more useful/meaningful view (e.g. `inbound 137.213.212.7` - * in the example above). - * see: https://www.elastic.co/guide/en/ecs/current/ecs-field-reference.html - * @param event The ResolverEvent to get the descriptive name for - * @returns { descriptiveName } An attempt at providing a readable name to the user - */ -export function descriptiveName(event: ResolverEvent): { subject: string; descriptor?: string } { - if (isLegacyEvent(event)) { - return { subject: eventName(event) }; - } - - // To be somewhat defensive, we'll check for the presence of these. - const partialEvent: DeepPartial = event; - - /** - * This list of attempts can be expanded/adjusted as the underlying model changes over time: - */ - - // Stable, per ECS 1.5: https://www.elastic.co/guide/en/ecs/current/ecs-allowed-values-event-category.html - - if (partialEvent.network?.forwarded_ip) { - return { - subject: String(partialEvent.network?.forwarded_ip), - descriptor: String(partialEvent.network?.direction), - }; - } - - if (partialEvent.file?.path) { - return { - subject: String(partialEvent.file?.path), - }; - } - - // Extended categories (per ECS 1.5): - const pathOrKey = partialEvent.registry?.path || partialEvent.registry?.key; - if (pathOrKey) { - return { - subject: String(pathOrKey), - }; - } - - if (partialEvent.dns?.question?.name) { - return { subject: String(partialEvent.dns?.question?.name) }; +export function eventKind(event: SafeResolverEvent): string[] { + if (isLegacyEventSafeVersion(event)) { + return []; + } else { + return values(event.event?.kind); } - - // Fall back on entityId if we can't fish a more descriptive name out. - return { subject: entityId(event) }; } diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 6afec75903477..d97fdfbf7d186 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -183,7 +183,7 @@ export interface ResolverTree { relatedEvents: Omit; relatedAlerts: Omit; ancestry: ResolverAncestry; - lifecycle: ResolverEvent[]; + lifecycle: SafeResolverEvent[]; stats: ResolverNodeStats; } @@ -209,7 +209,7 @@ export interface SafeResolverTree { */ export interface ResolverLifecycleNode { entityID: string; - lifecycle: ResolverEvent[]; + lifecycle: SafeResolverEvent[]; /** * stats are only set when the entire tree is being fetched */ @@ -263,7 +263,7 @@ export interface SafeResolverAncestry { */ export interface ResolverRelatedEvents { entityID: string; - events: ResolverEvent[]; + events: SafeResolverEvent[]; nextEvent: string | null; } diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts index 0883a3787fcce..fd086bd9b984e 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts @@ -9,7 +9,6 @@ import { ResolverTree, ResolverEntityIndex, } from '../../../../common/endpoint/types'; -import { mockEndpointEvent } from '../../mocks/endpoint_event'; import { mockTreeWithNoAncestorsAnd2Children } from '../../mocks/resolver_tree'; import { DataAccessLayer } from '../../types'; @@ -54,13 +53,7 @@ export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; me relatedEvents(entityID: string): Promise { return Promise.resolve({ entityID, - events: [ - mockEndpointEvent({ - entityID, - name: 'event', - timestamp: 0, - }), - ], + events: [], nextEvent: null, }); }, diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts index ec0fa93485783..86450b25eb1da 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts @@ -61,7 +61,7 @@ export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): { events: [ mockEndpointEvent({ entityID, - name: 'event', + processName: 'event', timestamp: 0, }), ], diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts index 95ec0cd1a5f77..ec773a09ae8e0 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts @@ -66,7 +66,7 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): { entityID, events, nextEvent: null, - } as ResolverRelatedEvents); + }); }, /** diff --git a/x-pack/plugins/security_solution/public/resolver/index.ts b/x-pack/plugins/security_solution/public/resolver/index.ts index 409f82c9d1560..08a3722f40493 100644 --- a/x-pack/plugins/security_solution/public/resolver/index.ts +++ b/x-pack/plugins/security_solution/public/resolver/index.ts @@ -7,7 +7,7 @@ import { Provider } from 'react-redux'; import { ResolverPluginSetup } from './types'; import { resolverStoreFactory } from './store/index'; import { ResolverWithoutProviders } from './view/resolver_without_providers'; -import { noAncestorsTwoChildren } from './data_access_layer/mocks/no_ancestors_two_children'; +import { noAncestorsTwoChildrenWithRelatedEventsOnOrigin } from './data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin'; /** * These exports are used by the plugin 'resolverTest' defined in x-pack's plugin_functional suite. @@ -23,7 +23,7 @@ export function resolverPluginSetup(): ResolverPluginSetup { ResolverWithoutProviders, mocks: { dataAccessLayer: { - noAncestorsTwoChildren, + noAncestorsTwoChildrenWithRelatedEventsOnOrigin, }, }, }; diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts b/x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts index 083f6b8baa59f..d19ca285ff3ff 100644 --- a/x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts +++ b/x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts @@ -4,31 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EndpointEvent } from '../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../common/endpoint/types'; /** * Simple mock endpoint event that works for tree layouts. */ export function mockEndpointEvent({ entityID, - name, - parentEntityId, - timestamp, - lifecycleType, + processName = 'process name', + parentEntityID, + timestamp = 0, + eventType = 'start', + eventCategory = 'process', pid = 0, + eventID = 'event id', }: { entityID: string; - name: string; - parentEntityId?: string; - timestamp: number; - lifecycleType?: string; + processName?: string; + parentEntityID?: string; + timestamp?: number; + eventType?: string; + eventCategory?: string; pid?: number; -}): EndpointEvent { + eventID?: string; +}): SafeResolverEvent { return { '@timestamp': timestamp, event: { - type: lifecycleType ? lifecycleType : 'start', - category: 'process', + type: eventType, + category: eventCategory, + id: eventID, }, agent: { id: 'agent.id', @@ -46,15 +51,15 @@ export function mockEndpointEvent({ entity_id: entityID, executable: 'executable', args: 'args', - name, + name: processName, pid, hash: { md5: 'hash.md5', }, parent: { pid: 0, - entity_id: parentEntityId, + entity_id: parentEntityID, }, }, - } as EndpointEvent; + }; } diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts index 8bd5953e9cb41..8691ecac4d1cc 100644 --- a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts +++ b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts @@ -5,7 +5,8 @@ */ import { mockEndpointEvent } from './endpoint_event'; -import { ResolverTree, ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types'; +import { ResolverTree, SafeResolverEvent } from '../../../common/endpoint/types'; +import * as eventModel from '../../../common/endpoint/models/event'; export function mockTreeWith2AncestorsAndNoChildren({ originID, @@ -16,34 +17,42 @@ export function mockTreeWith2AncestorsAndNoChildren({ firstAncestorID: string; originID: string; }): ResolverTree { - const secondAncestor: ResolverEvent = mockEndpointEvent({ + const secondAncestor: SafeResolverEvent = mockEndpointEvent({ entityID: secondAncestorID, - name: 'a', - parentEntityId: 'none', + processName: 'a', + parentEntityID: 'none', timestamp: 0, }); - const firstAncestor: ResolverEvent = mockEndpointEvent({ + const firstAncestor: SafeResolverEvent = mockEndpointEvent({ entityID: firstAncestorID, - name: 'b', - parentEntityId: secondAncestorID, + processName: 'b', + parentEntityID: secondAncestorID, timestamp: 1, }); - const originEvent: ResolverEvent = mockEndpointEvent({ + const originEvent: SafeResolverEvent = mockEndpointEvent({ entityID: originID, - name: 'c', - parentEntityId: firstAncestorID, + processName: 'c', + parentEntityID: firstAncestorID, timestamp: 2, }); - return ({ + return { entityID: originID, children: { childNodes: [], + nextChild: null, }, ancestry: { - ancestors: [{ lifecycle: [secondAncestor] }, { lifecycle: [firstAncestor] }], + nextAncestor: null, + ancestors: [ + { entityID: secondAncestorID, lifecycle: [secondAncestor] }, + { entityID: firstAncestorID, lifecycle: [firstAncestor] }, + ], }, lifecycle: [originEvent], - } as unknown) as ResolverTree; + relatedEvents: { events: [], nextEvent: null }, + relatedAlerts: { alerts: [], nextAlert: null }, + stats: { events: { total: 2, byCategory: {} }, totalAlerts: 0 }, + }; } export function mockTreeWithAllProcessesTerminated({ @@ -55,44 +64,44 @@ export function mockTreeWithAllProcessesTerminated({ firstAncestorID: string; originID: string; }): ResolverTree { - const secondAncestor: ResolverEvent = mockEndpointEvent({ + const secondAncestor: SafeResolverEvent = mockEndpointEvent({ entityID: secondAncestorID, - name: 'a', - parentEntityId: 'none', + processName: 'a', + parentEntityID: 'none', timestamp: 0, }); - const firstAncestor: ResolverEvent = mockEndpointEvent({ + const firstAncestor: SafeResolverEvent = mockEndpointEvent({ entityID: firstAncestorID, - name: 'b', - parentEntityId: secondAncestorID, + processName: 'b', + parentEntityID: secondAncestorID, timestamp: 1, }); - const originEvent: ResolverEvent = mockEndpointEvent({ + const originEvent: SafeResolverEvent = mockEndpointEvent({ entityID: originID, - name: 'c', - parentEntityId: firstAncestorID, + processName: 'c', + parentEntityID: firstAncestorID, timestamp: 2, }); - const secondAncestorTermination: ResolverEvent = mockEndpointEvent({ + const secondAncestorTermination: SafeResolverEvent = mockEndpointEvent({ entityID: secondAncestorID, - name: 'a', - parentEntityId: 'none', + processName: 'a', + parentEntityID: 'none', timestamp: 0, - lifecycleType: 'end', + eventType: 'end', }); - const firstAncestorTermination: ResolverEvent = mockEndpointEvent({ + const firstAncestorTermination: SafeResolverEvent = mockEndpointEvent({ entityID: firstAncestorID, - name: 'b', - parentEntityId: secondAncestorID, + processName: 'b', + parentEntityID: secondAncestorID, timestamp: 1, - lifecycleType: 'end', + eventType: 'end', }); - const originEventTermination: ResolverEvent = mockEndpointEvent({ + const originEventTermination: SafeResolverEvent = mockEndpointEvent({ entityID: originID, - name: 'c', - parentEntityId: firstAncestorID, + processName: 'c', + parentEntityID: firstAncestorID, timestamp: 2, - lifecycleType: 'end', + eventType: 'end', }); return ({ entityID: originID, @@ -109,26 +118,10 @@ export function mockTreeWithAllProcessesTerminated({ } as unknown) as ResolverTree; } -/** - * A valid category for a related event. E.g. "registry", "network", "file" - */ -type RelatedEventCategory = string; -/** - * A valid type for a related event. E.g. "start", "end", "access" - */ -type RelatedEventType = string; - /** * Add/replace related event info (on origin node) for any mock ResolverTree - * - * @param treeToAddRelatedEventsTo the ResolverTree to modify - * @param relatedEventsToAddByCategoryAndType Iterable of `[category, type]` pairs describing related events. e.g. [['dns','info'],['registry','access']] */ -function withRelatedEventsOnOrigin( - treeToAddRelatedEventsTo: ResolverTree, - relatedEventsToAddByCategoryAndType: Iterable<[RelatedEventCategory, RelatedEventType]> -): ResolverTree { - const events: SafeResolverEvent[] = []; +function withRelatedEventsOnOrigin(tree: ResolverTree, events: SafeResolverEvent[]): ResolverTree { const byCategory: Record = {}; const stats = { totalAlerts: 0, @@ -137,29 +130,19 @@ function withRelatedEventsOnOrigin( byCategory, }, }; - for (const [category, type] of relatedEventsToAddByCategoryAndType) { - events.push({ - '@timestamp': 1, - event: { - kind: 'event', - type, - category, - id: 'xyz', - }, - process: { - entity_id: treeToAddRelatedEventsTo.entityID, - }, - }); + for (const event of events) { stats.events.total++; - stats.events.byCategory[category] = stats.events.byCategory[category] - ? stats.events.byCategory[category] + 1 - : 1; + for (const category of eventModel.eventCategory(event)) { + stats.events.byCategory[category] = stats.events.byCategory[category] + ? stats.events.byCategory[category] + 1 + : 1; + } } return { - ...treeToAddRelatedEventsTo, + ...tree, stats, relatedEvents: { - events: events as ResolverEvent[], + events, nextEvent: null, }, }; @@ -174,38 +157,46 @@ export function mockTreeWithNoAncestorsAnd2Children({ firstChildID: string; secondChildID: string; }): ResolverTree { - const origin: ResolverEvent = mockEndpointEvent({ + const origin: SafeResolverEvent = mockEndpointEvent({ pid: 0, entityID: originID, - name: 'c.ext', - parentEntityId: 'none', + processName: 'c.ext', + parentEntityID: 'none', timestamp: 0, }); - const firstChild: ResolverEvent = mockEndpointEvent({ + const firstChild: SafeResolverEvent = mockEndpointEvent({ pid: 1, entityID: firstChildID, - name: 'd', - parentEntityId: originID, + processName: 'd', + parentEntityID: originID, timestamp: 1, }); - const secondChild: ResolverEvent = mockEndpointEvent({ + const secondChild: SafeResolverEvent = mockEndpointEvent({ pid: 2, entityID: secondChildID, - name: 'e', - parentEntityId: originID, + processName: 'e', + parentEntityID: originID, timestamp: 2, }); - return ({ + return { entityID: originID, children: { - childNodes: [{ lifecycle: [firstChild] }, { lifecycle: [secondChild] }], + childNodes: [ + { entityID: firstChildID, lifecycle: [firstChild] }, + { entityID: secondChildID, lifecycle: [secondChild] }, + ], + nextChild: null, }, ancestry: { ancestors: [], + nextAncestor: null, }, lifecycle: [origin], - } as unknown) as ResolverTree; + relatedEvents: { events: [], nextEvent: null }, + relatedAlerts: { alerts: [], nextAlert: null }, + stats: { events: { total: 2, byCategory: {} }, totalAlerts: 0 }, + }; } /** @@ -222,52 +213,52 @@ export function mockTreeWith1AncestorAnd2ChildrenAndAllNodesHave2GraphableEvents firstChildID: string; secondChildID: string; }): ResolverTree { - const ancestor: ResolverEvent = mockEndpointEvent({ + const ancestor: SafeResolverEvent = mockEndpointEvent({ entityID: ancestorID, - name: ancestorID, + processName: ancestorID, timestamp: 1, - parentEntityId: undefined, + parentEntityID: undefined, }); - const ancestorClone: ResolverEvent = mockEndpointEvent({ + const ancestorClone: SafeResolverEvent = mockEndpointEvent({ entityID: ancestorID, - name: ancestorID, + processName: ancestorID, timestamp: 1, - parentEntityId: undefined, + parentEntityID: undefined, }); - const origin: ResolverEvent = mockEndpointEvent({ + const origin: SafeResolverEvent = mockEndpointEvent({ entityID: originID, - name: originID, - parentEntityId: ancestorID, + processName: originID, + parentEntityID: ancestorID, timestamp: 0, }); - const originClone: ResolverEvent = mockEndpointEvent({ + const originClone: SafeResolverEvent = mockEndpointEvent({ entityID: originID, - name: originID, - parentEntityId: ancestorID, + processName: originID, + parentEntityID: ancestorID, timestamp: 0, }); - const firstChild: ResolverEvent = mockEndpointEvent({ + const firstChild: SafeResolverEvent = mockEndpointEvent({ entityID: firstChildID, - name: firstChildID, - parentEntityId: originID, + processName: firstChildID, + parentEntityID: originID, timestamp: 1, }); - const firstChildClone: ResolverEvent = mockEndpointEvent({ + const firstChildClone: SafeResolverEvent = mockEndpointEvent({ entityID: firstChildID, - name: firstChildID, - parentEntityId: originID, + processName: firstChildID, + parentEntityID: originID, timestamp: 1, }); - const secondChild: ResolverEvent = mockEndpointEvent({ + const secondChild: SafeResolverEvent = mockEndpointEvent({ entityID: secondChildID, - name: secondChildID, - parentEntityId: originID, + processName: secondChildID, + parentEntityID: originID, timestamp: 2, }); - const secondChildClone: ResolverEvent = mockEndpointEvent({ + const secondChildClone: SafeResolverEvent = mockEndpointEvent({ entityID: secondChildID, - name: secondChildID, - parentEntityId: originID, + processName: secondChildID, + parentEntityID: originID, timestamp: 2, }); @@ -330,9 +321,22 @@ export function mockTreeWithNoAncestorsAndTwoChildrenAndRelatedEventsOnOrigin({ firstChildID, secondChildID, }); - const withRelatedEvents: Array<[string, string]> = [ - ['registry', 'access'], - ['registry', 'access'], + const parentEntityID = eventModel.parentEntityIDSafeVersion(baseTree.lifecycle[0]); + const relatedEvents = [ + mockEndpointEvent({ + entityID: originID, + parentEntityID, + eventID: 'first related event', + eventType: 'access', + eventCategory: 'registry', + }), + mockEndpointEvent({ + entityID: originID, + parentEntityID, + eventID: 'second related event', + eventType: 'access', + eventCategory: 'registry', + }), ]; - return withRelatedEventsOnOrigin(baseTree, withRelatedEvents); + return withRelatedEventsOnOrigin(baseTree, relatedEvents); } diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts index 4d48b34fb2841..380b15cf9da4c 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts @@ -6,11 +6,7 @@ import { eventType, orderByTime, userInfoForProcess } from './process_event'; import { mockProcessEvent } from './process_event_test_helpers'; -import { - LegacyEndpointEvent, - ResolverEvent, - SafeResolverEvent, -} from '../../../common/endpoint/types'; +import { LegacyEndpointEvent, SafeResolverEvent } from '../../../common/endpoint/types'; describe('process event', () => { describe('eventType', () => { @@ -45,7 +41,7 @@ describe('process event', () => { }); }); describe('orderByTime', () => { - let mock: (time: number, eventID: string) => ResolverEvent; + let mock: (time: number, eventID: string) => SafeResolverEvent; let events: SafeResolverEvent[]; beforeEach(() => { mock = (time, eventID) => { @@ -54,20 +50,20 @@ describe('process event', () => { event: { id: eventID, }, - } as ResolverEvent; + }; }; // 2 events each for numbers -1, 0, 1, and NaN // each event has a unique id, a through h // order is arbitrary events = [ - mock(-1, 'a') as SafeResolverEvent, - mock(0, 'c') as SafeResolverEvent, - mock(1, 'e') as SafeResolverEvent, - mock(NaN, 'g') as SafeResolverEvent, - mock(-1, 'b') as SafeResolverEvent, - mock(0, 'd') as SafeResolverEvent, - mock(1, 'f') as SafeResolverEvent, - mock(NaN, 'h') as SafeResolverEvent, + mock(-1, 'a'), + mock(0, 'c'), + mock(1, 'e'), + mock(NaN, 'g'), + mock(-1, 'b'), + mock(0, 'd'), + mock(1, 'f'), + mock(NaN, 'h'), ]; }); it('sorts events as expected', () => { diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts index ea588731a55c8..1510fc7f9f365 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as event from '../../../common/endpoint/models/event'; +import { firstNonNullValue } from '../../../common/endpoint/models/ecs_safety_helpers'; + +import * as eventModel from '../../../common/endpoint/models/event'; import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types'; import { ResolverProcessType } from '../types'; @@ -12,19 +14,11 @@ import { ResolverProcessType } from '../types'; * Returns true if the process's eventType is either 'processCreated' or 'processRan'. * Resolver will only render 'graphable' process events. */ -export function isGraphableProcess(passedEvent: ResolverEvent) { +export function isGraphableProcess(passedEvent: SafeResolverEvent) { return eventType(passedEvent) === 'processCreated' || eventType(passedEvent) === 'processRan'; } -function isValue(field: string | string[], value: string) { - if (field instanceof Array) { - return field.length === 1 && field[0] === value; - } else { - return field === value; - } -} - -export function isTerminatedProcess(passedEvent: ResolverEvent) { +export function isTerminatedProcess(passedEvent: SafeResolverEvent) { return eventType(passedEvent) === 'processTerminated'; } @@ -33,7 +27,7 @@ export function isTerminatedProcess(passedEvent: ResolverEvent) { * may return NaN if the timestamp wasn't present or was invalid. */ export function datetime(passedEvent: SafeResolverEvent): number | null { - const timestamp = event.timestampSafeVersion(passedEvent); + const timestamp = eventModel.timestampSafeVersion(passedEvent); const time = timestamp === undefined ? 0 : new Date(timestamp).getTime(); @@ -44,8 +38,8 @@ export function datetime(passedEvent: SafeResolverEvent): number | null { /** * Returns a custom event type for a process event based on the event's metadata. */ -export function eventType(passedEvent: ResolverEvent): ResolverProcessType { - if (event.isLegacyEvent(passedEvent)) { +export function eventType(passedEvent: SafeResolverEvent): ResolverProcessType { + if (eventModel.isLegacyEventSafeVersion(passedEvent)) { const { endgame: { event_type_full: type, event_subtype_full: subType }, } = passedEvent; @@ -64,20 +58,20 @@ export function eventType(passedEvent: ResolverEvent): ResolverProcessType { return 'processCausedAlert'; } } else { - const { - event: { type, category, kind }, - } = passedEvent; - if (isValue(category, 'process')) { - if (isValue(type, 'start') || isValue(type, 'change') || isValue(type, 'creation')) { + const type = new Set(eventModel.eventType(passedEvent)); + const category = new Set(eventModel.eventCategory(passedEvent)); + const kind = new Set(eventModel.eventKind(passedEvent)); + if (category.has('process')) { + if (type.has('start') || type.has('change') || type.has('creation')) { return 'processCreated'; - } else if (isValue(type, 'info')) { + } else if (type.has('info')) { return 'processRan'; - } else if (isValue(type, 'end')) { + } else if (type.has('end')) { return 'processTerminated'; } else { return 'unknownProcessEvent'; } - } else if (kind === 'alert') { + } else if (kind.has('alert')) { return 'processCausedAlert'; } } @@ -88,7 +82,7 @@ export function eventType(passedEvent: ResolverEvent): ResolverProcessType { * Returns the process event's PID */ export function uniquePidForProcess(passedEvent: ResolverEvent): string { - if (event.isLegacyEvent(passedEvent)) { + if (eventModel.isLegacyEvent(passedEvent)) { return String(passedEvent.endgame.unique_pid); } else { return passedEvent.process.entity_id; @@ -98,45 +92,32 @@ export function uniquePidForProcess(passedEvent: ResolverEvent): string { /** * Returns the PID for the process on the host */ -export function processPid(passedEvent: ResolverEvent): number | undefined { - if (event.isLegacyEvent(passedEvent)) { - return passedEvent.endgame.pid; - } else { - return passedEvent.process.pid; - } +export function processPID(event: SafeResolverEvent): number | undefined { + return firstNonNullValue( + eventModel.isLegacyEventSafeVersion(event) ? event.endgame.pid : event.process?.pid + ); } /** * Returns the process event's parent PID */ export function uniqueParentPidForProcess(passedEvent: ResolverEvent): string | undefined { - if (event.isLegacyEvent(passedEvent)) { + if (eventModel.isLegacyEvent(passedEvent)) { return String(passedEvent.endgame.unique_ppid); } else { return passedEvent.process.parent?.entity_id; } } -/** - * Returns the process event's parent PID - */ -export function processParentPid(passedEvent: ResolverEvent): number | undefined { - if (event.isLegacyEvent(passedEvent)) { - return passedEvent.endgame.ppid; - } else { - return passedEvent.process.parent?.pid; - } -} - /** * Returns the process event's path on its host */ -export function processPath(passedEvent: ResolverEvent): string | undefined { - if (event.isLegacyEvent(passedEvent)) { - return passedEvent.endgame.process_path; - } else { - return passedEvent.process.executable; - } +export function processPath(passedEvent: SafeResolverEvent): string | undefined { + return firstNonNullValue( + eventModel.isLegacyEventSafeVersion(passedEvent) + ? passedEvent.endgame.process_path + : passedEvent.process?.executable + ); } /** @@ -148,19 +129,6 @@ export function userInfoForProcess( return passedEvent.user; } -/** - * Returns the MD5 hash for the `passedEvent` parameter, or undefined if it can't be located - * @param {ResolverEvent} passedEvent The `ResolverEvent` to get the MD5 value for - * @returns {string | undefined} The MD5 string for the event - */ -export function md5HashForProcess(passedEvent: ResolverEvent): string | undefined { - if (event.isLegacyEvent(passedEvent)) { - // There is not currently a key for this on Legacy event types - return undefined; - } - return passedEvent?.process?.hash?.md5; -} - /** * Returns the command line path and arguments used to run the `passedEvent` if any * @@ -168,7 +136,7 @@ export function md5HashForProcess(passedEvent: ResolverEvent): string | undefine * @returns {string | undefined} The arguments (including the path) used to run the process */ export function argsForProcess(passedEvent: ResolverEvent): string | undefined { - if (event.isLegacyEvent(passedEvent)) { + if (eventModel.isLegacyEvent(passedEvent)) { // There is not currently a key for this on Legacy event types return undefined; } @@ -184,8 +152,8 @@ export function orderByTime(first: SafeResolverEvent, second: SafeResolverEvent) if (firstDatetime === secondDatetime) { // break ties using an arbitrary (stable) comparison of `eventId` (which should be unique) - return String(event.eventIDSafeVersion(first)).localeCompare( - String(event.eventIDSafeVersion(second)) + return String(eventModel.eventIDSafeVersion(first)).localeCompare( + String(eventModel.eventIDSafeVersion(second)) ); } else if (firstDatetime === null || secondDatetime === null) { // sort `null`'s as higher than numbers diff --git a/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts index 446e371832d38..775b88246b61f 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts @@ -6,12 +6,12 @@ import { ResolverTree, - ResolverEvent, ResolverNodeStats, ResolverLifecycleNode, ResolverChildNode, + SafeResolverEvent, } from '../../../common/endpoint/types'; -import { uniquePidForProcess } from './process_event'; +import * as eventModel from '../../../common/endpoint/models/event'; /** * ResolverTree is a type returned by the server. @@ -29,7 +29,7 @@ function lifecycleNodes(tree: ResolverTree): ResolverLifecycleNode[] { * All the process events */ export function lifecycleEvents(tree: ResolverTree) { - const events: ResolverEvent[] = [...tree.lifecycle]; + const events: SafeResolverEvent[] = [...tree.lifecycle]; for (const { lifecycle } of tree.children.childNodes) { events.push(...lifecycle); } @@ -66,7 +66,7 @@ export function mock({ /** * Events represented by the ResolverTree. */ - events: ResolverEvent[]; + events: SafeResolverEvent[]; children?: ResolverChildNode[]; /** * Optionally provide cursors for the 'children' and 'ancestry' edges. @@ -77,8 +77,12 @@ export function mock({ return null; } const first = events[0]; + const entityID = eventModel.entityIDSafeVersion(first); + if (!entityID) { + throw new Error('first mock event must include an entityID.'); + } return { - entityID: uniquePidForProcess(first), + entityID, // Required children: { childNodes: children, diff --git a/x-pack/plugins/security_solution/public/resolver/store/actions.ts b/x-pack/plugins/security_solution/public/resolver/store/actions.ts index 6a02d5b76bc4c..3348c962efdea 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/actions.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/actions.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { CameraAction } from './camera'; -import { ResolverEvent } from '../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../common/endpoint/types'; import { DataAction } from './data/action'; /** @@ -16,25 +16,7 @@ interface UserBroughtProcessIntoView { /** * Used to identify the process node that should be brought into view. */ - readonly process: ResolverEvent; - /** - * The time (since epoch in milliseconds) when the action was dispatched. - */ - readonly time: number; - }; -} - -/** - * When an examination of query params in the UI indicates that state needs to - * be updated to reflect the new selection - */ -interface AppDetectedNewIdFromQueryParams { - readonly type: 'appDetectedNewIdFromQueryParams'; - readonly payload: { - /** - * Used to identify the process the process that should be synced with state. - */ - readonly process: ResolverEvent; + readonly process: SafeResolverEvent; /** * The time (since epoch in milliseconds) when the action was dispatched. */ @@ -51,15 +33,6 @@ interface UserRequestedRelatedEventData { readonly payload: string; } -/** - * The action dispatched when the app requests related event data for one - * subject (whose entity_id should be included as `payload`) - */ -interface AppDetectedMissingEventData { - readonly type: 'appDetectedMissingEventData'; - readonly payload: string; -} - /** * When the user switches the "active descendant" of the Resolver. * The "active descendant" (from the point of view of the parent element) @@ -127,6 +100,4 @@ export type ResolverAction = | UserBroughtProcessIntoView | UserFocusedOnResolverNode | UserSelectedResolverNode - | UserRequestedRelatedEventData - | AppDetectedNewIdFromQueryParams - | AppDetectedMissingEventData; + | UserRequestedRelatedEventData; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts index 59d1494ae8c27..0cb1cd1cec771 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts @@ -45,14 +45,6 @@ interface AppAbortedResolverDataRequest { readonly payload: TreeFetcherParameters; } -/** - * Will occur when a request for related event data is unsuccessful. - */ -interface ServerFailedToReturnRelatedEventData { - readonly type: 'serverFailedToReturnRelatedEventData'; - readonly payload: string; -} - /** * When related events are returned from the server */ @@ -64,7 +56,6 @@ interface ServerReturnedRelatedEventData { export type DataAction = | ServerReturnedResolverData | ServerFailedToReturnResolverData - | ServerFailedToReturnRelatedEventData | ServerReturnedRelatedEventData | AppRequestedResolverData | AppAbortedResolverDataRequest; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts index 1e2de06ea4af5..5714345de0431 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts @@ -10,8 +10,7 @@ import { dataReducer } from './reducer'; import * as selectors from './selectors'; import { DataState } from '../../types'; import { DataAction } from './action'; -import { ResolverChildNode, ResolverEvent, ResolverTree } from '../../../../common/endpoint/types'; -import * as eventModel from '../../../../common/endpoint/models/event'; +import { ResolverChildNode, ResolverTree } from '../../../../common/endpoint/types'; import { values } from '../../../../common/endpoint/models/ecs_safety_helpers'; import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters'; @@ -43,7 +42,7 @@ describe('Resolver Data Middleware', () => { const tree = mockResolverTree({ // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with // a lot of the frontend functions. So casting it back to the unsafe type for now. - events: baseTree.allEvents as ResolverEvent[], + events: baseTree.allEvents, cursors: { childrenNextChild: 'aValidChildCursor', ancestryNextAncestor: 'aValidAncestorCursor', @@ -61,9 +60,6 @@ describe('Resolver Data Middleware', () => { describe('when data was received with stats mocked for the first child node', () => { let firstChildNodeInTree: TreeNode; - let eventStatsForFirstChildNode: { total: number; byCategory: Record }; - let categoryToOverCount: string; - let aggregateCategoryTotalForFirstChildNode: number; let tree: ResolverTree; /** @@ -73,13 +69,7 @@ describe('Resolver Data Middleware', () => { */ beforeEach(() => { - ({ - tree, - firstChildNodeInTree, - eventStatsForFirstChildNode, - categoryToOverCount, - aggregateCategoryTotalForFirstChildNode, - } = mockedTree()); + ({ tree, firstChildNodeInTree } = mockedTree()); if (tree) { dispatchTree(tree); } @@ -94,7 +84,7 @@ describe('Resolver Data Middleware', () => { entityID: firstChildNodeInTree.id, // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with // a lot of the frontend functions. So casting it back to the unsafe type for now. - events: firstChildNodeInTree.relatedEvents as ResolverEvent[], + events: firstChildNodeInTree.relatedEvents, nextEvent: null, }, }; @@ -108,121 +98,6 @@ describe('Resolver Data Middleware', () => { expect(selectedEventsForFirstChildNode).toBe(firstChildNodeInTree.relatedEvents); }); - it('should indicate the correct related event count for each category', () => { - const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState()); - const displayCountsForCategory = selectedRelatedInfo(firstChildNodeInTree.id) - ?.numberActuallyDisplayedForCategory!; - - const eventCategoriesForNode: string[] = Object.keys( - eventStatsForFirstChildNode.byCategory - ); - - for (const eventCategory of eventCategoriesForNode) { - expect(`${eventCategory}:${displayCountsForCategory(eventCategory)}`).toBe( - `${eventCategory}:${eventStatsForFirstChildNode.byCategory[eventCategory]}` - ); - } - }); - /** - * The general approach reflected here is to _avoid_ showing a limit warning - even if we hit - * the overall related event limit - as long as the number in our category matches what the stats - * say we have. E.g. If the stats say you have 20 dns events, and we receive 20 dns events, we - * don't need to display a limit warning for that, even if we hit some overall event limit of e.g. 100 - * while we were fetching the 20. - */ - it('should not indicate the limit has been exceeded because the number of related events received for the category is greater or equal to the stats count', () => { - const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState()); - const shouldShowLimit = selectedRelatedInfo(firstChildNodeInTree.id) - ?.shouldShowLimitForCategory!; - for (const typeCounted of Object.keys(eventStatsForFirstChildNode.byCategory)) { - expect(shouldShowLimit(typeCounted)).toBe(false); - } - }); - it('should not indicate that there are any related events missing because the number of related events received for the category is greater or equal to the stats count', () => { - const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState()); - const notDisplayed = selectedRelatedInfo(firstChildNodeInTree.id) - ?.numberNotDisplayedForCategory!; - for (const typeCounted of Object.keys(eventStatsForFirstChildNode.byCategory)) { - expect(notDisplayed(typeCounted)).toBe(0); - } - }); - it('should return an overall correct count for the number of related events', () => { - const aggregateTotalByEntityId = selectors.relatedEventAggregateTotalByEntityId( - store.getState() - ); - const countForId = aggregateTotalByEntityId(firstChildNodeInTree.id); - expect(countForId).toBe(aggregateCategoryTotalForFirstChildNode); - }); - }); - describe('when data was received and stats show more related events than the API can provide', () => { - beforeEach(() => { - // Add 1 to the stats for an event category so that the selectors think we are missing data. - // This mutates `tree`, and then we re-dispatch it - eventStatsForFirstChildNode.byCategory[categoryToOverCount] = - eventStatsForFirstChildNode.byCategory[categoryToOverCount] + 1; - - if (tree) { - dispatchTree(tree); - const relatedAction: DataAction = { - type: 'serverReturnedRelatedEventData', - payload: { - entityID: firstChildNodeInTree.id, - // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with - // a lot of the frontend functions. So casting it back to the unsafe type for now. - events: firstChildNodeInTree.relatedEvents as ResolverEvent[], - nextEvent: 'aValidNextEventCursor', - }, - }; - store.dispatch(relatedAction); - } - }); - it('should have the correct related events', () => { - const selectedEventsByEntityId = selectors.relatedEventsByEntityId(store.getState()); - const selectedEventsForFirstChildNode = selectedEventsByEntityId.get( - firstChildNodeInTree.id - )!.events; - - expect(selectedEventsForFirstChildNode).toBe(firstChildNodeInTree.relatedEvents); - }); - it('should return related events for the category equal to the number of events of that type provided', () => { - const relatedEventsByCategory = selectors.relatedEventsByCategory(store.getState()); - const relatedEventsForOvercountedCategory = relatedEventsByCategory( - firstChildNodeInTree.id - )(categoryToOverCount); - expect(relatedEventsForOvercountedCategory.length).toBe( - eventStatsForFirstChildNode.byCategory[categoryToOverCount] - 1 - ); - }); - it('should return the correct related event detail metadata for a given related event', () => { - const relatedEventsByCategory = selectors.relatedEventsByCategory(store.getState()); - const someRelatedEventForTheFirstChild = relatedEventsByCategory(firstChildNodeInTree.id)( - categoryToOverCount - )[0]; - const relatedEventID = eventModel.eventId(someRelatedEventForTheFirstChild)!; - const relatedDisplayInfo = selectors.relatedEventDisplayInfoByEntityAndSelfID( - store.getState() - )(firstChildNodeInTree.id, relatedEventID); - const [, countOfSameType, , sectionData] = relatedDisplayInfo; - const hostEntries = sectionData.filter((section) => { - return section.sectionTitle === 'host'; - })[0].entries; - expect(hostEntries).toContainEqual({ title: 'os.platform', description: 'Windows' }); - expect(countOfSameType).toBe( - eventStatsForFirstChildNode.byCategory[categoryToOverCount] - 1 - ); - }); - it('should indicate the limit has been exceeded because the number of related events received for the category is less than what the stats count said it would be', () => { - const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState()); - const shouldShowLimit = selectedRelatedInfo(firstChildNodeInTree.id) - ?.shouldShowLimitForCategory!; - expect(shouldShowLimit(categoryToOverCount)).toBe(true); - }); - it('should indicate that there are related events missing because the number of related events received for the category is less than what the stats count said it would be', () => { - const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState()); - const notDisplayed = selectedRelatedInfo(firstChildNodeInTree.id) - ?.numberNotDisplayedForCategory!; - expect(notDisplayed(categoryToOverCount)).toBe(1); - }); }); }); }); @@ -241,7 +116,7 @@ function mockedTree() { const tree = mockResolverTree({ // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with // a lot of the frontend functions. So casting it back to the unsafe type for now. - events: baseTree.allEvents as ResolverEvent[], + events: baseTree.allEvents, /** * Calculate children from the ResolverTree response using the children of the `Tree` we generated using the Resolver data generator code. * Compile (and attach) stats to the first child node. @@ -255,7 +130,7 @@ function mockedTree() { const childNode: Partial = {}; // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with // a lot of the frontend functions. So casting it back to the unsafe type for now. - childNode.lifecycle = node.lifecycle as ResolverEvent[]; + childNode.lifecycle = node.lifecycle; // `TreeNode` has `id` which is the same as `entityID`. // The `ResolverChildNode` calls the entityID as `entityID`. @@ -281,8 +156,6 @@ function mockedTree() { return { tree: tree!, firstChildNodeInTree, - eventStatsForFirstChildNode: statsResults.eventStats, - aggregateCategoryTotalForFirstChildNode: statsResults.aggregateCategoryTotal, categoryToOverCount: statsResults.firstCategory, }; } @@ -309,7 +182,6 @@ function compileStatsForChild( }; /** The category of the first event. */ firstCategory: string; - aggregateCategoryTotal: number; } { const totalRelatedEvents = node.relatedEvents.length; // For the purposes of testing, we pick one category to fake an extra event for @@ -317,12 +189,6 @@ function compileStatsForChild( let firstCategory: string | undefined; - // This is the "aggregate total" which is displayed to users as the total count - // of related events for the node. It is tallied by incrementing for every discrete - // event.category in an event.category array (or just 1 for a plain string). E.g. two events - // categories 'file' and ['dns','network'] would have an `aggregate total` of 3. - let aggregateCategoryTotal: number = 0; - const compiledStats = node.relatedEvents.reduce( (counts: Record, relatedEvent) => { // get an array of categories regardless of whether category is a string or string[] @@ -336,7 +202,6 @@ function compileStatsForChild( // Increment the count of events with this category counts[category] = counts[category] ? counts[category] + 1 : 1; - aggregateCategoryTotal++; } return counts; }, @@ -354,6 +219,5 @@ function compileStatsForChild( byCategory: compiledStats, }, firstCategory, - aggregateCategoryTotal, }; } diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts index c8df95aaee6f4..1819407a19516 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts @@ -11,9 +11,7 @@ import * as treeFetcherParameters from '../../models/tree_fetcher_parameters'; const initialState: DataState = { relatedEvents: new Map(), - relatedEventsReady: new Map(), resolverComponentInstanceID: undefined, - tree: {}, }; export const dataReducer: Reducer = (state = initialState, action) => { @@ -44,7 +42,7 @@ export const dataReducer: Reducer = (state = initialS }; return nextState; } else if (action.type === 'appAbortedResolverDataRequest') { - if (treeFetcherParameters.equal(action.payload, state.tree.pendingRequestParameters)) { + if (treeFetcherParameters.equal(action.payload, state.tree?.pendingRequestParameters)) { // the request we were awaiting was aborted const nextState: DataState = { ...state, @@ -81,7 +79,7 @@ export const dataReducer: Reducer = (state = initialS return nextState; } else if (action.type === 'serverFailedToReturnResolverData') { /** Only handle this if we are expecting a response */ - if (state.tree.pendingRequestParameters !== undefined) { + if (state.tree?.pendingRequestParameters !== undefined) { const nextState: DataState = { ...state, tree: { @@ -97,19 +95,9 @@ export const dataReducer: Reducer = (state = initialS } else { return state; } - } else if ( - action.type === 'userRequestedRelatedEventData' || - action.type === 'appDetectedMissingEventData' - ) { - const nextState: DataState = { - ...state, - relatedEventsReady: new Map([...state.relatedEventsReady, [action.payload, false]]), - }; - return nextState; } else if (action.type === 'serverReturnedRelatedEventData') { const nextState: DataState = { ...state, - relatedEventsReady: new Map([...state.relatedEventsReady, [action.payload.entityID, true]]), relatedEvents: new Map([...state.relatedEvents, [action.payload.entityID, action.payload]]), }; return nextState; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts index 539325faffdf0..d9717b52d9ce1 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts @@ -16,7 +16,7 @@ import { mockTreeWithAllProcessesTerminated, mockTreeWithNoProcessEvents, } from '../../mocks/resolver_tree'; -import { uniquePidForProcess } from '../../models/process_event'; +import * as eventModel from '../../../../common/endpoint/models/event'; import { EndpointEvent } from '../../../../common/endpoint/types'; import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters'; @@ -411,7 +411,7 @@ describe('data state', () => { expect(graphables.length).toBe(3); for (const event of graphables) { expect(() => { - selectors.ariaFlowtoCandidate(state())(uniquePidForProcess(event)); + selectors.ariaFlowtoCandidate(state())(eventModel.entityIDSafeVersion(event)!); }).not.toThrow(); } }); diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index d714ddb181470..fe7e8b5f22f1c 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -14,46 +14,36 @@ import { IndexedProcessNode, AABB, VisibleEntites, - SectionData, TreeFetcherParameters, } from '../../types'; -import { - isGraphableProcess, - isTerminatedProcess, - uniquePidForProcess, - uniqueParentPidForProcess, -} from '../../models/process_event'; +import { isGraphableProcess, isTerminatedProcess } from '../../models/process_event'; import * as indexedProcessTreeModel from '../../models/indexed_process_tree'; +import * as eventModel from '../../../../common/endpoint/models/event'; import { - ResolverEvent, ResolverTree, ResolverNodeStats, ResolverRelatedEvents, SafeResolverEvent, - EndpointEvent, - LegacyEndpointEvent, } from '../../../../common/endpoint/types'; import * as resolverTreeModel from '../../models/resolver_tree'; import * as treeFetcherParametersModel from '../../models/tree_fetcher_parameters'; import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout'; -import * as eventModel from '../../../../common/endpoint/models/event'; import * as vector2 from '../../models/vector2'; -import { formatDate } from '../../view/panels/panel_content_utilities'; /** * If there is currently a request. */ export function isTreeLoading(state: DataState): boolean { - return state.tree.pendingRequestParameters !== undefined; + return state.tree?.pendingRequestParameters !== undefined; } /** * If a request was made and it threw an error or returned a failure response code. */ export function hadErrorLoadingTree(state: DataState): boolean { - if (state.tree.lastResponse) { - return !state.tree.lastResponse.successful; + if (state.tree?.lastResponse) { + return !state.tree?.lastResponse.successful; } return false; } @@ -70,7 +60,7 @@ export function resolverComponentInstanceID(state: DataState): string { * we're currently interested in. */ const resolverTreeResponse = (state: DataState): ResolverTree | undefined => { - return state.tree.lastResponse?.successful ? state.tree.lastResponse.result : undefined; + return state.tree?.lastResponse?.successful ? state.tree?.lastResponse.result : undefined; }; /** @@ -102,7 +92,7 @@ export const terminatedProcesses = createSelector(resolverTreeResponse, function .lifecycleEvents(tree) .filter(isTerminatedProcess) .map((terminatedEvent) => { - return uniquePidForProcess(terminatedEvent); + return eventModel.entityIDSafeVersion(terminatedEvent); }) ); }); @@ -115,8 +105,8 @@ export const isProcessTerminated = createSelector(terminatedProcesses, function terminatedProcesses /* eslint-enable no-shadow */ ) { - return (entityId: string) => { - return terminatedProcesses.has(entityId); + return (entityID: string) => { + return terminatedProcesses.has(entityID); }; }); @@ -125,12 +115,14 @@ export const isProcessTerminated = createSelector(terminatedProcesses, function */ export const graphableProcesses = createSelector(resolverTreeResponse, function (tree?) { // Keep track of the last process event (in array order) for each entity ID - const events: Map = new Map(); + const events: Map = new Map(); if (tree) { for (const event of resolverTreeModel.lifecycleEvents(tree)) { if (isGraphableProcess(event)) { - const entityID = uniquePidForProcess(event); - events.set(entityID, event); + const entityID = eventModel.entityIDSafeVersion(event); + if (entityID !== undefined) { + events.set(entityID, event); + } } } return [...events.values()]; @@ -147,7 +139,7 @@ export const tree = createSelector(graphableProcesses, function indexedTree( graphableProcesses /* eslint-enable no-shadow */ ) { - return indexedProcessTreeModel.factory(graphableProcesses as SafeResolverEvent[]); + return indexedProcessTreeModel.factory(graphableProcesses); }); /** @@ -169,24 +161,18 @@ export const relatedEventsStats: ( ); /** - * This returns the "aggregate total" for related events, tallied as the sum - * of their individual `event.category`s. E.g. a [DNS, Network] would count as two - * towards the aggregate total. + * The total number of events related to a node. */ -export const relatedEventAggregateTotalByEntityId: ( +export const relatedEventTotalCount: ( state: DataState -) => (entityId: string) => number = createSelector(relatedEventsStats, (relatedStats) => { - return (entityId) => { - const statsForEntity = relatedStats(entityId); - if (statsForEntity === undefined) { - return 0; - } - return Object.values(statsForEntity?.events?.byCategory || {}).reduce( - (sum, val) => sum + val, - 0 - ); - }; -}); +) => (entityID: string) => number | undefined = createSelector( + relatedEventsStats, + (relatedStats) => { + return (entityID) => { + return relatedStats(entityID)?.events?.total; + }; + } +); /** * returns a map of entity_ids to related event data. @@ -197,98 +183,36 @@ export function relatedEventsByEntityId(data: DataState): Map
` entries - * @deprecated + * Get an event (from memory) by its `event.id`. + * @deprecated Use the API to find events by ID */ -const objectToDescriptionListEntries = function* ( - obj: object, - prefix = '' -): Generator<{ title: string; description: string }> { - const nextPrefix = prefix.length ? `${prefix}.` : ''; - for (const [metaKey, metaValue] of Object.entries(obj)) { - if (typeof metaValue === 'number' || typeof metaValue === 'string') { - yield { title: nextPrefix + metaKey, description: `${metaValue}` }; - } else if (metaValue instanceof Array) { - yield { - title: nextPrefix + metaKey, - description: metaValue - .filter((arrayEntry) => { - return typeof arrayEntry === 'number' || typeof arrayEntry === 'string'; - }) - .join(','), - }; - } else if (typeof metaValue === 'object') { - yield* objectToDescriptionListEntries(metaValue, nextPrefix + metaKey); +export const eventByID = createSelector(relatedEventsByEntityId, (relatedEvents) => { + // A map of nodeID to a map of eventID to events. Lazily populated. + const memo = new Map>(); + return ({ eventID, nodeID }: { eventID: string; nodeID: string }) => { + // We keep related events in a map by their nodeID. + const eventsWrapper = relatedEvents.get(nodeID); + if (!eventsWrapper) { + return undefined; } - } -}; - -/** - * Returns a function that returns the information needed to display related event details based on - * the related event's entityID and its own ID. - * @deprecated - */ -export const relatedEventDisplayInfoByEntityAndSelfID: ( - state: DataState -) => ( - entityId: string, - relatedEventId: string | number -) => [ - EndpointEvent | LegacyEndpointEvent | undefined, - number, - string | undefined, - SectionData, - string -] = createSelector(relatedEventsByEntityId, function relatedEventDetails( - /* eslint-disable no-shadow */ - relatedEventsByEntityId - /* eslint-enable no-shadow */ -) { - return defaultMemoize((entityId: string, relatedEventId: string | number) => { - const relatedEventsForThisProcess = relatedEventsByEntityId.get(entityId); - if (!relatedEventsForThisProcess) { - return [undefined, 0, undefined, [], '']; - } - const specificEvent = relatedEventsForThisProcess.events.find( - (evt) => eventModel.eventId(evt) === relatedEventId - ); - // For breadcrumbs: - const specificCategory = specificEvent && eventModel.primaryEventCategory(specificEvent); - const countOfCategory = relatedEventsForThisProcess.events.reduce((sumtotal, evt) => { - return eventModel.primaryEventCategory(evt) === specificCategory ? sumtotal + 1 : sumtotal; - }, 0); - // Assuming these details (agent, ecs, process) aren't as helpful, can revisit - const { agent, ecs, process, ...relevantData } = specificEvent as SafeResolverEvent & { - // Type this with various unknown keys so that ts will let us delete those keys - ecs: unknown; - process: unknown; - }; - - let displayDate = ''; - const sectionData: SectionData = Object.entries(relevantData) - .map(([sectionTitle, val]) => { - if (sectionTitle === '@timestamp') { - displayDate = formatDate(val); - return { sectionTitle: '', entries: [] }; + // When an event from a nodeID is requested, build a map for all events related to that node. + if (!memo.has(nodeID)) { + const map = new Map(); + for (const event of eventsWrapper.events) { + const id = eventModel.eventIDSafeVersion(event); + if (id !== undefined) { + map.set(id, event); } - if (typeof val !== 'object') { - return { sectionTitle, entries: [{ title: sectionTitle, description: `${val}` }] }; - } - return { sectionTitle, entries: [...objectToDescriptionListEntries(val)] }; - }) - .filter((v) => v.sectionTitle !== '' && v.entries.length); - - return [specificEvent, countOfCategory, specificCategory, sectionData, displayDate]; - }); + } + memo.set(nodeID, map); + } + const eventMap = memo.get(nodeID); + if (!eventMap) { + // This shouldn't be possible. + return undefined; + } + return eventMap.get(eventID); + }; }); /** @@ -298,44 +222,65 @@ export const relatedEventDisplayInfoByEntityAndSelfID: ( */ export const relatedEventsByCategory: ( state: DataState -) => (entityID: string) => (ecsCategory: string) => ResolverEvent[] = createSelector( +) => (node: string, eventCategory: string) => SafeResolverEvent[] = createSelector( relatedEventsByEntityId, function ( /* eslint-disable no-shadow */ relatedEventsByEntityId /* eslint-enable no-shadow */ ) { - return defaultMemoize((entityId: string) => { - return defaultMemoize((ecsCategory: string) => { - const relatedById = relatedEventsByEntityId.get(entityId); - // With no related events, we can't return related by category - if (!relatedById) { - return []; + // A map of nodeID -> event category -> SafeResolverEvent[] + const nodeMap: Map> = new Map(); + for (const [nodeID, events] of relatedEventsByEntityId) { + // A map of eventCategory -> SafeResolverEvent[] + let categoryMap = nodeMap.get(nodeID); + if (!categoryMap) { + categoryMap = new Map(); + nodeMap.set(nodeID, categoryMap); + } + + for (const event of events.events) { + for (const category of eventModel.eventCategory(event)) { + let eventsInCategory = categoryMap.get(category); + if (!eventsInCategory) { + eventsInCategory = []; + categoryMap.set(category, eventsInCategory); + } + eventsInCategory.push(event); } - return relatedById.events.reduce( - (eventsByCategory: ResolverEvent[], candidate: ResolverEvent) => { - if ( - [candidate && eventModel.allEventCategories(candidate)].flat().includes(ecsCategory) - ) { - eventsByCategory.push(candidate); - } - return eventsByCategory; - }, - [] - ); - }); - }); + } + } + + // Use the same empty array for all values that are missing + const emptyArray: SafeResolverEvent[] = []; + + return (entityID: string, category: string): SafeResolverEvent[] => { + const categoryMap = nodeMap.get(entityID); + if (!categoryMap) { + return emptyArray; + } + const eventsInCategory = categoryMap.get(category); + return eventsInCategory ?? emptyArray; + }; } ); -/** - * returns a map of entity_ids to booleans indicating if it is waiting on related event - * A value of `undefined` can be interpreted as `not yet requested` - * @deprecated - */ -export function relatedEventsReady(data: DataState): Map { - return data.relatedEventsReady; -} +export const relatedEventCountByType: ( + state: DataState +) => (nodeID: string, eventType: string) => number | undefined = createSelector( + relatedEventsStats, + (statsMap) => { + return (nodeID: string, eventType: string): number | undefined => { + const stats = statsMap(nodeID); + if (stats) { + const value = Object.prototype.hasOwnProperty.call(stats.events.byCategory, eventType); + if (typeof value === 'number') { + return value; + } + } + }; + } +); /** * `true` if there were more children than we got in the last request. @@ -355,113 +300,6 @@ export function hasMoreAncestors(state: DataState): boolean { return resolverTree ? resolverTreeModel.hasMoreAncestors(resolverTree) : false; } -interface RelatedInfoFunctions { - shouldShowLimitForCategory: (category: string) => boolean; - numberNotDisplayedForCategory: (category: string) => number; - numberActuallyDisplayedForCategory: (category: string) => number; -} -/** - * A map of `entity_id`s to functions that provide information about - * related events by ECS `.category` Primarily to avoid having business logic - * in UI components. - * @deprecated - */ -export const relatedEventInfoByEntityId: ( - state: DataState -) => (entityID: string) => RelatedInfoFunctions | null = createSelector( - relatedEventsByEntityId, - relatedEventsStats, - function selectLineageLimitInfo( - /* eslint-disable no-shadow */ - relatedEventsByEntityId, - relatedEventsStats - /* eslint-enable no-shadow */ - ) { - return (entityId) => { - const stats = relatedEventsStats(entityId); - if (!stats) { - return null; - } - const eventsResponseForThisEntry = relatedEventsByEntityId.get(entityId); - const hasMoreEvents = - eventsResponseForThisEntry && eventsResponseForThisEntry.nextEvent !== null; - /** - * Get the "aggregate" total for the event category (i.e. _all_ events that would qualify as being "in category") - * For a set like `[DNS,File][File,DNS][Registry]` The first and second events would contribute to the aggregate total for DNS being 2. - * This is currently aligned with how the backed provides this information. - * - * @param eventCategory {string} The ECS category like 'file','dns',etc. - */ - const aggregateTotalForCategory = (eventCategory: string): number => { - return stats.events.byCategory[eventCategory] || 0; - }; - - /** - * Get all the related events in the category provided. - * - * @param eventCategory {string} The ECS category like 'file','dns',etc. - */ - const unmemoizedMatchingEventsForCategory = (eventCategory: string): ResolverEvent[] => { - if (!eventsResponseForThisEntry) { - return []; - } - return eventsResponseForThisEntry.events.filter((resolverEvent) => { - for (const category of [eventModel.allEventCategories(resolverEvent)].flat()) { - if (category === eventCategory) { - return true; - } - } - return false; - }); - }; - - const matchingEventsForCategory = unmemoizedMatchingEventsForCategory; - - /** - * The number of events that occurred before the API limit was reached. - * The number of events that came back form the API that have `eventCategory` in their list of categories. - * - * @param eventCategory {string} The ECS category like 'file','dns',etc. - */ - const numberActuallyDisplayedForCategory = (eventCategory: string): number => { - return matchingEventsForCategory(eventCategory)?.length || 0; - }; - - /** - * The total number counted by the backend - the number displayed - * - * @param eventCategory {string} The ECS category like 'file','dns',etc. - */ - const numberNotDisplayedForCategory = (eventCategory: string): number => { - return ( - aggregateTotalForCategory(eventCategory) - - numberActuallyDisplayedForCategory(eventCategory) - ); - }; - - /** - * `true` when the `nextEvent` cursor appeared in the results and we are short on the number needed to - * fullfill the aggregate count. - * - * @param eventCategory {string} The ECS category like 'file','dns',etc. - */ - const shouldShowLimitForCategory = (eventCategory: string): boolean => { - if (hasMoreEvents && numberNotDisplayedForCategory(eventCategory) > 0) { - return true; - } - return false; - }; - - const entryValue = { - shouldShowLimitForCategory, - numberNotDisplayedForCategory, - numberActuallyDisplayedForCategory, - }; - return entryValue; - }; - } -); - /** * If the tree resource needs to be fetched then these are the parameters that should be used. */ @@ -470,14 +308,14 @@ export function treeParametersToFetch(state: DataState): TreeFetcherParameters | * If there are current tree parameters that don't match the parameters used in the pending request (if there is a pending request) and that don't match the parameters used in the last completed request (if there was a last completed request) then we need to fetch the tree resource using the current parameters. */ if ( - state.tree.currentParameters !== undefined && + state.tree?.currentParameters !== undefined && !treeFetcherParametersModel.equal( - state.tree.currentParameters, - state.tree.lastResponse?.parameters + state.tree?.currentParameters, + state.tree?.lastResponse?.parameters ) && !treeFetcherParametersModel.equal( - state.tree.currentParameters, - state.tree.pendingRequestParameters + state.tree?.currentParameters, + state.tree?.pendingRequestParameters ) ) { return state.tree.currentParameters; @@ -533,10 +371,11 @@ export const layout = createSelector( */ export const processEventForID: ( state: DataState -) => (nodeID: string) => ResolverEvent | null = createSelector( +) => (nodeID: string) => SafeResolverEvent | null = createSelector( tree, - (indexedProcessTree) => (nodeID: string) => - indexedProcessTreeModel.processEvent(indexedProcessTree, nodeID) as ResolverEvent + (indexedProcessTree) => (nodeID: string) => { + return indexedProcessTreeModel.processEvent(indexedProcessTree, nodeID); + } ); /** @@ -547,7 +386,7 @@ export const ariaLevel: (state: DataState) => (nodeID: string) => number | null processEventForID, ({ ariaLevels }, processEventGetter) => (nodeID: string) => { const node = processEventGetter(nodeID); - return node ? ariaLevels.get(node as SafeResolverEvent) ?? null : null; + return node ? ariaLevels.get(node) ?? null : null; } ); @@ -582,7 +421,7 @@ export const ariaFlowtoCandidate: ( * Getting the following sibling of a node has an `O(n)` time complexity where `n` is the number of children the parent of the node has. * For this reason, we calculate the following siblings of the node and all of its siblings at once and cache them. */ - const nodeEvent: ResolverEvent | null = eventGetter(nodeID); + const nodeEvent: SafeResolverEvent | null = eventGetter(nodeID); if (!nodeEvent) { // this should never happen. @@ -592,23 +431,30 @@ export const ariaFlowtoCandidate: ( // nodes with the same parent ID const children = indexedProcessTreeModel.children( indexedProcessTree, - uniqueParentPidForProcess(nodeEvent) + eventModel.parentEntityIDSafeVersion(nodeEvent) ); - let previousChild: ResolverEvent | null = null; + let previousChild: SafeResolverEvent | null = null; // Loop over all nodes that have the same parent ID (even if the parent ID is undefined or points to a node that isn't in the tree.) for (const child of children) { if (previousChild !== null) { // Set the `child` as the following sibling of `previousChild`. - memo.set(uniquePidForProcess(previousChild), uniquePidForProcess(child as ResolverEvent)); + const previousChildEntityID = eventModel.entityIDSafeVersion(previousChild); + const followingSiblingEntityID = eventModel.entityIDSafeVersion(child); + if (previousChildEntityID !== undefined && followingSiblingEntityID !== undefined) { + memo.set(previousChildEntityID, followingSiblingEntityID); + } } // Set the child as the previous child. - previousChild = child as ResolverEvent; + previousChild = child; } if (previousChild) { // if there is a previous child, it has no following sibling. - memo.set(uniquePidForProcess(previousChild), null); + const entityID = eventModel.entityIDSafeVersion(previousChild); + if (entityID !== undefined) { + memo.set(entityID, null); + } } return memoizedGetter(nodeID); @@ -708,10 +554,10 @@ export function treeRequestParametersToAbort(state: DataState): TreeFetcherParam * If there is a pending request, and its not for the current parameters (even, if the current parameters are undefined) then we should abort the request. */ if ( - state.tree.pendingRequestParameters !== undefined && + state.tree?.pendingRequestParameters !== undefined && !treeFetcherParametersModel.equal( - state.tree.pendingRequestParameters, - state.tree.currentParameters + state.tree?.pendingRequestParameters, + state.tree?.currentParameters ) ) { return state.tree.pendingRequestParameters; @@ -725,19 +571,19 @@ export function treeRequestParametersToAbort(state: DataState): TreeFetcherParam */ export const relatedEventTotalForProcess: ( state: DataState -) => (event: ResolverEvent) => number | null = createSelector( +) => (event: SafeResolverEvent) => number | null = createSelector( relatedEventsStats, (statsForProcess) => { - return (event: ResolverEvent) => { - const stats = statsForProcess(uniquePidForProcess(event)); - if (!stats) { + return (event: SafeResolverEvent) => { + const nodeID = eventModel.entityIDSafeVersion(event); + if (nodeID === undefined) { return null; } - let total = 0; - for (const value of Object.values(stats.events.byCategory)) { - total += value; + const stats = statsForProcess(nodeID); + if (!stats) { + return null; } - return total; + return stats.events.total; }; } ); diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts index 28948debae891..506acefe51676 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts @@ -8,7 +8,7 @@ import { Store, createStore } from 'redux'; import { ResolverAction } from '../actions'; import { resolverReducer } from '../reducer'; import { ResolverState } from '../../types'; -import { LegacyEndpointEvent, ResolverEvent } from '../../../../common/endpoint/types'; +import { LegacyEndpointEvent, SafeResolverEvent } from '../../../../common/endpoint/types'; import { visibleNodesAndEdgeLines } from '../selectors'; import { mockProcessEvent } from '../../models/process_event_test_helpers'; import { mock as mockResolverTree } from '../../models/resolver_tree'; @@ -102,7 +102,7 @@ describe('resolver visible entities', () => { }); describe('when rendering a large tree with a small viewport', () => { beforeEach(() => { - const events: ResolverEvent[] = [ + const events: SafeResolverEvent[] = [ processA, processB, processC, @@ -130,7 +130,7 @@ describe('resolver visible entities', () => { }); describe('when rendering a large tree with a large viewport', () => { beforeEach(() => { - const events: ResolverEvent[] = [ + const events: SafeResolverEvent[] = [ processA, processB, processC, diff --git a/x-pack/plugins/security_solution/public/resolver/store/methods.ts b/x-pack/plugins/security_solution/public/resolver/store/methods.ts index 8dd15b1a44d0c..f121b2aa86888 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/methods.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/methods.ts @@ -7,7 +7,7 @@ import { animatePanning } from './camera/methods'; import { layout } from './selectors'; import { ResolverState } from '../types'; -import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../common/endpoint/types'; const animationDuration = 1000; @@ -17,10 +17,10 @@ const animationDuration = 1000; export function animateProcessIntoView( state: ResolverState, startTime: number, - process: ResolverEvent + process: SafeResolverEvent ): ResolverState { const { processNodePositions } = layout(state); - const position = processNodePositions.get(process as SafeResolverEvent); + const position = processNodePositions.get(process); if (position) { return { ...state, diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts index ef6b1f5eb3c6f..5dca858b4fabe 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts @@ -6,9 +6,10 @@ import { Dispatch, MiddlewareAPI } from 'redux'; import { ResolverState, DataAccessLayer } from '../../types'; -import { ResolverRelatedEvents } from '../../../../common/endpoint/types'; import { ResolverTreeFetcher } from './resolver_tree_fetcher'; + import { ResolverAction } from '../actions'; +import { RelatedEventsFetcher } from './related_events_fetcher'; type MiddlewareFactory = ( dataAccessLayer: DataAccessLayer @@ -25,33 +26,12 @@ type MiddlewareFactory = ( export const resolverMiddlewareFactory: MiddlewareFactory = (dataAccessLayer: DataAccessLayer) => { return (api) => (next) => { const resolverTreeFetcher = ResolverTreeFetcher(dataAccessLayer, api); + const relatedEventsFetcher = RelatedEventsFetcher(dataAccessLayer, api); return async (action: ResolverAction) => { next(action); resolverTreeFetcher(); - - if ( - action.type === 'userRequestedRelatedEventData' || - action.type === 'appDetectedMissingEventData' - ) { - const entityIdToFetchFor = action.payload; - let result: ResolverRelatedEvents | undefined; - try { - result = await dataAccessLayer.relatedEvents(entityIdToFetchFor); - } catch { - api.dispatch({ - type: 'serverFailedToReturnRelatedEventData', - payload: action.payload, - }); - } - - if (result) { - api.dispatch({ - type: 'serverReturnedRelatedEventData', - payload: result, - }); - } - } + relatedEventsFetcher(); }; }; }; diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts new file mode 100644 index 0000000000000..b83e3cff90736 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Dispatch, MiddlewareAPI } from 'redux'; +import { isEqual } from 'lodash'; +import { ResolverRelatedEvents } from '../../../../common/endpoint/types'; + +import { ResolverState, DataAccessLayer, PanelViewAndParameters } from '../../types'; +import * as selectors from '../selectors'; +import { ResolverAction } from '../actions'; + +export function RelatedEventsFetcher( + dataAccessLayer: DataAccessLayer, + api: MiddlewareAPI, ResolverState> +): () => void { + let last: PanelViewAndParameters | undefined; + + // Call this after each state change. + // This fetches the ResolverTree for the current entityID + // if the entityID changes while + return async () => { + const state = api.getState(); + + const newParams = selectors.panelViewAndParameters(state); + const oldParams = last; + // Update this each time before fetching data (or even if we don't fetch data) so that subsequent actions that call this (concurrently) will have up to date info. + last = newParams; + + // If the panel view params have changed and the current panel view is either `nodeEventsOfType` or `eventDetail`, then fetch the related events for that nodeID. + if ( + !isEqual(newParams, oldParams) && + (newParams.panelView === 'nodeEventsOfType' || newParams.panelView === 'eventDetail') + ) { + const nodeID = newParams.panelParameters.nodeID; + + const result: ResolverRelatedEvents | undefined = await dataAccessLayer.relatedEvents(nodeID); + + if (result) { + api.dispatch({ + type: 'serverReturnedRelatedEventData', + payload: result, + }); + } + } + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/store/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/reducer.ts index bf62fd0e60df8..ae1e9a58a2097 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/reducer.ts @@ -9,7 +9,7 @@ import { cameraReducer } from './camera/reducer'; import { dataReducer } from './data/reducer'; import { ResolverAction } from './actions'; import { ResolverState, ResolverUIState } from '../types'; -import { uniquePidForProcess } from '../models/process_event'; +import * as eventModel from '../../../common/endpoint/models/event'; const uiReducer: Reducer = ( state = { @@ -37,17 +37,18 @@ const uiReducer: Reducer = ( selectedNode: action.payload, }; return next; - } else if ( - action.type === 'userBroughtProcessIntoView' || - action.type === 'appDetectedNewIdFromQueryParams' - ) { - const nodeID = uniquePidForProcess(action.payload.process); - const next: ResolverUIState = { - ...state, - ariaActiveDescendant: nodeID, - selectedNode: nodeID, - }; - return next; + } else if (action.type === 'userBroughtProcessIntoView') { + const nodeID = eventModel.entityIDSafeVersion(action.payload.process); + if (nodeID !== undefined) { + const next: ResolverUIState = { + ...state, + ariaActiveDescendant: nodeID, + selectedNode: nodeID, + }; + return next; + } else { + return state; + } } else if (action.type === 'appReceivedNewExternalProperties') { const next: ResolverUIState = { ...state, @@ -68,10 +69,7 @@ const concernReducers = combineReducers({ export const resolverReducer: Reducer = (state, action) => { const nextState = concernReducers(state, action); - if ( - action.type === 'userBroughtProcessIntoView' || - action.type === 'appDetectedNewIdFromQueryParams' - ) { + if (action.type === 'userBroughtProcessIntoView') { return animateProcessIntoView(nextState, action.payload.time, action.payload.process); } else { return nextState; diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 96b080206b61e..3c99a186ac0c2 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -9,7 +9,7 @@ import * as cameraSelectors from './camera/selectors'; import * as dataSelectors from './data/selectors'; import * as uiSelectors from './ui/selectors'; import { ResolverState, IsometricTaxiLayout } from '../types'; -import { ResolverEvent, ResolverNodeStats } from '../../../common/endpoint/types'; +import { ResolverNodeStats, SafeResolverEvent } from '../../../common/endpoint/types'; import { entityIDSafeVersion } from '../../../common/endpoint/models/event'; /** @@ -61,6 +61,11 @@ export const isProcessTerminated = composeSelectors( dataSelectors.isProcessTerminated ); +/** + * Retrieve an event from memory using the event's ID. + */ +export const eventByID = composeSelectors(dataStateSelector, dataSelectors.eventByID); + /** * Given a nodeID (aka entity_id) get the indexed process event. * Legacy functions take process events instead of nodeID, use this to get @@ -68,7 +73,7 @@ export const isProcessTerminated = composeSelectors( */ export const processEventForID: ( state: ResolverState -) => (nodeID: string) => ResolverEvent | null = composeSelectors( +) => (nodeID: string) => SafeResolverEvent | null = composeSelectors( dataStateSelector, dataSelectors.processEventForID ); @@ -119,30 +124,27 @@ export const relatedEventsStats: ( * of their individual `event.category`s. E.g. a [DNS, Network] would count as two * towards the aggregate total. */ -export const relatedEventAggregateTotalByEntityId: ( +export const relatedEventTotalCount: ( state: ResolverState -) => (nodeID: string) => number = composeSelectors( +) => (nodeID: string) => number | undefined = composeSelectors( dataStateSelector, - dataSelectors.relatedEventAggregateTotalByEntityId + dataSelectors.relatedEventTotalCount ); -/** - * Map of related events... by entity id - * @deprecated - */ -export const relatedEventsByEntityId = composeSelectors( +export const relatedEventCountByType: ( + state: ResolverState +) => (nodeID: string, eventType: string) => number | undefined = composeSelectors( dataStateSelector, - dataSelectors.relatedEventsByEntityId + dataSelectors.relatedEventCountByType ); /** - * Returns a function that returns the information needed to display related event details based on - * the related event's entityID and its own ID. + * Map of related events... by entity id * @deprecated */ -export const relatedEventDisplayInfoByEntityAndSelfId = composeSelectors( +export const relatedEventsByEntityId = composeSelectors( dataStateSelector, - dataSelectors.relatedEventDisplayInfoByEntityAndSelfID + dataSelectors.relatedEventsByEntityId ); /** @@ -155,26 +157,6 @@ export const relatedEventsByCategory = composeSelectors( dataSelectors.relatedEventsByCategory ); -/** - * Entity ids to booleans for waiting status - * @deprecated - */ -export const relatedEventsReady = composeSelectors( - dataStateSelector, - dataSelectors.relatedEventsReady -); - -/** - * Business logic lookup functions by ECS category by entity id. - * Example usage: - * const numberOfFileEvents = infoByEntityId.get(`someEntityId`)?.getAggregateTotalForCategory(`file`); - * @deprecated - */ -export const relatedEventInfoByEntityId = composeSelectors( - dataStateSelector, - dataSelectors.relatedEventInfoByEntityId -); - /** * Returns the id of the "current" tree node (fake-focused) */ diff --git a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts index 6bc41832b92f2..a8882d835fce1 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts @@ -8,9 +8,9 @@ import { decode, encode } from 'rison-node'; import { createSelector } from 'reselect'; import { PanelViewAndParameters, ResolverUIState } from '../../types'; -import { ResolverEvent } from '../../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { isPanelViewAndParameters } from '../../models/location_search'; -import { eventId } from '../../../../common/endpoint/models/event'; +import { eventID } from '../../../../common/endpoint/models/event'; /** * id of the "current" tree node (fake-focused) @@ -124,12 +124,12 @@ export const relatedEventDetailHrefs: ( ) => ( category: string, nodeID: string, - events: ResolverEvent[] + events: SafeResolverEvent[] ) => Map = createSelector(relativeHref, (relativeHref) => { - return (category: string, nodeID: string, events: ResolverEvent[]) => { + return (category: string, nodeID: string, events: SafeResolverEvent[]) => { const hrefsByEntityID = new Map(); events.map((event) => { - const entityID = String(eventId(event)); + const entityID = String(eventID(event)); const eventDetailPanelParams: PanelViewAndParameters = { panelView: 'eventDetail', panelParameters: { diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 952a1c5764d8e..4dc614abe3345 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -211,9 +211,8 @@ export interface TreeFetcherParameters { */ export interface DataState { readonly relatedEvents: Map; - readonly relatedEventsReady: Map; - readonly tree: { + readonly tree?: { /** * The parameters passed from the resolver properties */ @@ -614,8 +613,9 @@ export interface ResolverPluginSetup { dataAccessLayer: { /** * A mock `DataAccessLayer` that returns a tree that has no ancestor nodes but which has 2 children nodes. + * The origin has 2 related registry events */ - noAncestorsTwoChildren: () => { dataAccessLayer: DataAccessLayer }; + noAncestorsTwoChildrenWithRelatedEventsOnOrigin: () => { dataAccessLayer: DataAccessLayer }; }; }; } diff --git a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx index 53b889004798f..777a7292e9c23 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import React from 'react'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -53,7 +55,7 @@ const StyledElapsedTime = styled.div` /** * A placeholder line segment view that connects process nodes. */ -const EdgeLineComponent = React.memo( +export const EdgeLine = React.memo( ({ className, edgeLineMetadata, @@ -155,7 +157,3 @@ const EdgeLineComponent = React.memo( ); } ); - -EdgeLineComponent.displayName = 'EdgeLine'; - -export const EdgeLine = EdgeLineComponent; diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx index 75aecf6747cca..dbeca840a4b66 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + /* eslint-disable react/button-has-type */ import React, { useCallback, useMemo, useContext } from 'react'; @@ -54,7 +56,7 @@ const StyledGraphControls = styled.div` /** * Controls for zooming, panning, and centering in Resolver */ -const GraphControlsComponent = React.memo( +export const GraphControls = React.memo( ({ className, }: { @@ -204,7 +206,3 @@ const GraphControlsComponent = React.memo( ); } ); - -GraphControlsComponent.displayName = 'GraphControlsComponent'; - -export const GraphControls = GraphControlsComponent; diff --git a/x-pack/plugins/security_solution/public/resolver/view/limit_warnings.tsx b/x-pack/plugins/security_solution/public/resolver/view/limit_warnings.tsx index 3f2b7c769cad7..bc57c4e28b9cd 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/limit_warnings.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/limit_warnings.tsx @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import React from 'react'; -import { EuiCallOut } from '@elastic/eui'; import { FormattedMessage } from 'react-intl'; +import { LimitWarningsEuiCallOut } from './styles'; const lineageLimitMessage = ( ); -const LineageTitleMessage = React.memo(function LineageTitleMessage({ - numberOfEntries, -}: { - numberOfEntries: number; -}) { +const LineageTitleMessage = React.memo(function ({ numberOfEntries }: { numberOfEntries: number }) { return (

- + ); }); /** * Limit warning for hitting a limit of nodes in the tree */ -export const LimitWarning = React.memo(function LimitWarning({ - className, - numberDisplayed, -}: { - className?: string; - numberDisplayed: number; -}) { +export const LimitWarning = React.memo(function ({ numberDisplayed }: { numberDisplayed: number }) { return ( - } >

{lineageLimitMessage}

-
+ ); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/node.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/node.test.tsx new file mode 100644 index 0000000000000..0b381f6771f00 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/node.test.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { noAncestorsTwoChildren } from '../data_access_layer/mocks/no_ancestors_two_children'; +import { Simulator } from '../test_utilities/simulator'; +// Extend jest with a custom matcher +import '../test_utilities/extend_jest'; + +let simulator: Simulator; +let databaseDocumentID: string; + +// the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances +const resolverComponentInstanceID = 'resolverComponentInstanceID'; + +describe('Resolver, when analyzing a tree that has no ancestors and 2 children', () => { + beforeEach(async () => { + // create a mock data access layer + const { metadata: dataAccessLayerMetadata, dataAccessLayer } = noAncestorsTwoChildren(); + + // save a reference to the `_id` supported by the mock data layer + databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID; + + // create a resolver simulator, using the data access layer and an arbitrary component instance ID + simulator = new Simulator({ + databaseDocumentID, + dataAccessLayer, + resolverComponentInstanceID, + indices: [], + }); + }); + + it('shows 1 node with the words "Analyzed Event" in the label', async () => { + await expect( + simulator.map(() => { + return simulator.testSubject('resolver:node:description').map((element) => element.text()); + }) + ).toYieldEqualTo(['Analyzed Event · Running Process', 'Running Process', 'Running Process']); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx index 7cfbd9a794669..2f23469606aca 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx @@ -5,7 +5,7 @@ */ import { createMemoryHistory, History as HistoryPackageHistoryInterface } from 'history'; -import { noAncestorsTwoChildren } from '../data_access_layer/mocks/no_ancestors_two_children'; +import { noAncestorsTwoChildrenWithRelatedEventsOnOrigin } from '../data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin'; import { Simulator } from '../test_utilities/simulator'; // Extend jest with a custom matcher import '../test_utilities/extend_jest'; @@ -14,7 +14,7 @@ import { urlSearch } from '../test_utilities/url_search'; // the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances const resolverComponentInstanceID = 'resolverComponentInstanceID'; -describe(`Resolver: when analyzing a tree with no ancestors and two children, and when the component instance ID is ${resolverComponentInstanceID}`, () => { +describe(`Resolver: when analyzing a tree with no ancestors and two children and two related registry event on the origin, and when the component instance ID is ${resolverComponentInstanceID}`, () => { /** * Get (or lazily create and get) the simulator. */ @@ -32,7 +32,10 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an beforeEach(() => { // create a mock data access layer - const { metadata: dataAccessLayerMetadata, dataAccessLayer } = noAncestorsTwoChildren(); + const { + metadata: dataAccessLayerMetadata, + dataAccessLayer, + } = noAncestorsTwoChildrenWithRelatedEventsOnOrigin(); entityIDs = dataAccessLayerMetadata.entityIDs; @@ -184,6 +187,38 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an }) ); }); + describe("and when the user clicks the link to the node's events", () => { + beforeEach(async () => { + const nodeEventsListLink = await simulator().resolve( + 'resolver:node-detail:node-events-link' + ); + + if (nodeEventsListLink) { + nodeEventsListLink.simulate('click', { button: 0 }); + } + }); + it('should show a link to view 2 registry events', async () => { + await expect( + simulator().map(() => { + // The link text is split across two columns. The first column is the count and the second column has the type. + const type = simulator().testSubject('resolver:panel:node-events:event-type-count'); + const link = simulator().testSubject('resolver:panel:node-events:event-type-link'); + return { + typeLength: type.length, + linkLength: link.length, + typeText: type.text(), + linkText: link.text(), + }; + }) + ).toYieldEqualTo({ + typeLength: 1, + linkLength: 1, + linkText: 'registry', + // EUI's Table adds the column name to the value. + typeText: 'Count2', + }); + }); + }); describe('and when the node list link has been clicked', () => { beforeEach(async () => { const nodeListLink = await simulator().resolve( diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/breadcrumbs.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/breadcrumbs.tsx new file mode 100644 index 0000000000000..ed39198009364 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/breadcrumbs.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable react/display-name */ + +import { i18n } from '@kbn/i18n'; +import { EuiBreadcrumb, EuiBetaBadge } from '@elastic/eui'; +import React, { memo } from 'react'; +import { BetaHeader, ThemedBreadcrumbs } from './styles'; +import { useColors } from '../use_colors'; + +/** + * Breadcrumb menu + */ +export const Breadcrumbs = memo(function ({ breadcrumbs }: { breadcrumbs: EuiBreadcrumb[] }) { + const { resolverBreadcrumbBackground, resolverEdgeText } = useColors(); + return ( + <> + + + + + + ); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx index 4e9d64f5a76a4..cc5f39e985d9e 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx @@ -43,7 +43,7 @@ export const CubeForProcess = memo(function ({ className={className} width="2.15em" height="2.15em" - viewBox="0 0 100% 100%" + viewBox="0 0 34 34" data-test-subj={dataTestSubj} isOrigin={isOrigin} > diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.test.ts b/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.test.ts new file mode 100644 index 0000000000000..1c4e1f4199bc4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.test.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { deepObjectEntries } from './deep_object_entries'; + +describe('deepObjectEntries', () => { + const valuesAndExpected: Array<[ + objectValue: object, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expected: Array<[path: Array, fieldValue: unknown]> + ]> = [ + [{}, []], // No 'field' values found + [{ a: {} }, []], // No 'field' values found + [{ a: { b: undefined } }, []], // No 'field' values found + [{ a: { b: undefined, c: [] } }, []], // No 'field' values found + [{ a: { b: undefined, c: [null] } }, []], // No 'field' values found + [{ a: { b: undefined, c: [null, undefined, 1] } }, [[['a', 'c'], 1]]], // Only `1` is a non-null value. It is under `a.c` because we ignore array indices + [ + { a: { b: undefined, c: [null, undefined, 1, { d: ['e'] }] } }, + [ + // 1 and 'e' are valid fields. + [['a', 'c'], 1], + [['a', 'c', 'd'], 'e'], + ], + ], + ]; + + describe.each(valuesAndExpected)('when passed %j', (value, expected) => { + it(`should return ${JSON.stringify(expected)}`, () => { + expect(deepObjectEntries(value)).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.ts b/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.ts new file mode 100644 index 0000000000000..a508b00be5739 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/** + * Sort of like object entries, but does a DFS of an object. + * Instead of getting a key, an array of keys is returned. + * The array of keys represents the path to the value. + * `undefined` and `null` values are omitted. + */ +export function deepObjectEntries(root: object): Array<[path: string[], value: unknown]> { + const queue: Array<{ path: string[]; value: unknown }> = [{ path: [], value: root }]; + const result: Array<[path: string[], value: unknown]> = []; + while (queue.length) { + const next = queue.shift(); + if (next === undefined) { + // this should be impossible + throw new Error(); + } + const { path, value } = next; + if (Array.isArray(value)) { + // branch on arrays + queue.push( + ...value.map((element) => ({ + path: [...path], // unlike with object paths, don't add the number indices to `path` + value: element, + })) + ); + } else if (typeof value === 'object' && value !== null) { + // branch on non-null objects + queue.push( + ...Object.keys(value).map((key) => ({ + path: [...path, key], + value: (value as Record)[key], + })) + ); + } else if (value !== undefined && value !== null) { + // emit other non-null, defined, values + result.push([path, value]); + } + } + return result; +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.test.tsx new file mode 100644 index 0000000000000..e869ab1ecd456 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.test.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; +import { DescriptiveName } from './descriptive_name'; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; +import { mount, ReactWrapper } from 'enzyme'; +import { I18nProvider } from '@kbn/i18n/react'; + +describe('DescriptiveName', () => { + let generator: EndpointDocGenerator; + let wrapper: (event: SafeResolverEvent) => ReactWrapper; + beforeEach(() => { + generator = new EndpointDocGenerator('seed'); + wrapper = (event: SafeResolverEvent) => + mount( + + + + ); + }); + it('returns the right name for a registry event', () => { + const extensions = { registry: { key: `HKLM/Windows/Software/abc` } }; + const event = generator.generateEvent({ eventCategory: 'registry', extensions }); + expect(wrapper(event).text()).toEqual(`HKLM/Windows/Software/abc`); + }); + + it('returns the right name for a network event', () => { + const randomIP = `${generator.randomIP()}`; + const extensions = { network: { direction: 'outbound', forwarded_ip: randomIP } }; + const event = generator.generateEvent({ eventCategory: 'network', extensions }); + expect(wrapper(event).text()).toEqual(`outbound ${randomIP}`); + }); + + it('returns the right name for a file event', () => { + const extensions = { file: { path: 'C:\\My Documents\\business\\January\\processName' } }; + const event = generator.generateEvent({ eventCategory: 'file', extensions }); + expect(wrapper(event).text()).toEqual('C:\\My Documents\\business\\January\\processName'); + }); + + it('returns the right name for a dns event', () => { + const extensions = { dns: { question: { name: `${generator.randomIP()}` } } }; + const event = generator.generateEvent({ eventCategory: 'dns', extensions }); + expect(wrapper(event).text()).toEqual(extensions.dns.question.name); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.tsx new file mode 100644 index 0000000000000..195ebceee0610 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FormattedMessage } from 'react-intl'; + +import React from 'react'; + +import { + isLegacyEventSafeVersion, + processNameSafeVersion, + entityIDSafeVersion, +} from '../../../../common/endpoint/models/event'; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; + +/** + * Based on the ECS category of the event, attempt to provide a more descriptive name + * (e.g. the `event.registry.key` for `registry` or the `dns.question.name` for `dns`, etc.). + * This function returns the data in the form of `{subject, descriptor}` where `subject` will + * tend to be the more distinctive term (e.g. 137.213.212.7 for a network event) and the + * `descriptor` can be used to present more useful/meaningful view (e.g. `inbound 137.213.212.7` + * in the example above). + * see: https://www.elastic.co/guide/en/ecs/current/ecs-field-reference.html + * @param event The ResolverEvent to get the descriptive name for + */ +export function DescriptiveName({ event }: { event: SafeResolverEvent }) { + if (isLegacyEventSafeVersion(event)) { + return ( + + ); + } + + /** + * This list of attempts can be expanded/adjusted as the underlying model changes over time: + */ + + // Stable, per ECS 1.5: https://www.elastic.co/guide/en/ecs/current/ecs-allowed-values-event-category.html + + if (event.network?.forwarded_ip) { + return ( + + ); + } + + if (event.file?.path) { + return ( + + ); + } + + if (event.registry?.path) { + return ( + + ); + } + + if (event.registry?.key) { + return ( + + ); + } + + if (event.dns?.question?.name) { + return ( + + ); + } + return ( + + ); +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx index 24d2a4a8f43f0..72f0d54d51fa3 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx @@ -4,275 +4,103 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useMemo, useEffect, Fragment } from 'react'; +/* eslint-disable no-continue */ + +/* eslint-disable react/display-name */ + +import React, { memo, useMemo, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiText, EuiDescriptionList, EuiTextColor, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; import { useSelector } from 'react-redux'; import { FormattedMessage } from 'react-intl'; import { StyledPanel } from '../styles'; -import { StyledBreadcrumbs, BoldCode, StyledTime, GeneratedText } from './panel_content_utilities'; -import * as event from '../../../../common/endpoint/models/event'; +import { BoldCode, StyledTime, GeneratedText, formatDate } from './panel_content_utilities'; +import { Breadcrumbs } from './breadcrumbs'; +import * as eventModel from '../../../../common/endpoint/models/event'; import * as selectors from '../../store/selectors'; -import { useResolverDispatch } from '../use_resolver_dispatch'; -import { PanelContentError } from './panel_content_error'; import { PanelLoading } from './panel_loading'; import { ResolverState } from '../../types'; -import { useNavigateOrReplace } from '../use_navigate_or_replace'; - -// Adding some styles to prevent horizontal scrollbars, per request from UX review -const StyledDescriptionList = memo(styled(EuiDescriptionList)` - &.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title { - max-width: 8em; - overflow-wrap: break-word; - } - &.euiDescriptionList.euiDescriptionList--column dd.euiDescriptionList__description { - max-width: calc(100% - 8.5em); - overflow-wrap: break-word; - } -`); - -// Also prevents horizontal scrollbars on long descriptive names -const StyledDescriptiveName = memo(styled(EuiText)` - padding-right: 1em; - overflow-wrap: break-word; -`); - -// Styling subtitles, per UX review: -const StyledFlexTitle = memo(styled('h3')` - display: flex; - flex-flow: row; - font-size: 1.2em; -`); -const StyledTitleRule = memo(styled('hr')` - &.euiHorizontalRule.euiHorizontalRule--full.euiHorizontalRule--marginSmall.override { - display: block; - flex: 1; - margin-left: 0.5em; - } -`); +import { DescriptiveName } from './descriptive_name'; +import { useLinkProps } from '../use_link_props'; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; +import { deepObjectEntries } from './deep_object_entries'; -const TitleHr = memo(() => { - return ( - +export const EventDetail = memo(function EventDetail({ + nodeID, + eventID, + eventType, +}: { + nodeID: string; + eventID: string; + /** The event type to show in the breadcrumbs */ + eventType: string; +}) { + const event = useSelector((state: ResolverState) => + selectors.eventByID(state)({ nodeID, eventID }) ); + const processEvent = useSelector((state: ResolverState) => + selectors.processEventForID(state)(nodeID) + ); + if (event && processEvent) { + return ( + + ); + } else { + return ( + + + + ); + } }); -TitleHr.displayName = 'TitleHR'; - -/** - * Take description list entries and prepare them for display by - * seeding with `` tags. - * - * @param entries {title: string, description: string}[] - */ -function entriesForDisplay(entries: Array<{ title: string; description: string }>) { - return entries.map((entry) => { - return { - description: {entry.description}, - title: {entry.title}, - }; - }); -} /** * This view presents a detailed view of all the available data for a related event, split and titled by the "section" * it appears in the underlying ResolverEvent */ -export const EventDetail = memo(function ({ +const EventDetailContents = memo(function ({ nodeID, - eventID, + event, + eventType, + processEvent, }: { nodeID: string; - eventID: string; -}) { - const parentEvent = useSelector((state: ResolverState) => - selectors.processEventForID(state)(nodeID) - ); - - const relatedEventsStats = useSelector((state: ResolverState) => - selectors.relatedEventsStats(state)(nodeID) - ); - const countForParent: number = Object.values(relatedEventsStats?.events.byCategory || {}).reduce( - (sum, val) => sum + val, - 0 - ); - const processName = (parentEvent && event.eventName(parentEvent)) || '*'; - const processEntityId = (parentEvent && event.entityId(parentEvent)) || ''; - const totalCount = countForParent || 0; - const eventsString = i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.events', - { - defaultMessage: 'Events', - } - ); - const naString = i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.NA', - { - defaultMessage: 'N/A', - } - ); - - const relatedsReadyMap = useSelector(selectors.relatedEventsReady); - const relatedsReady = relatedsReadyMap.get(processEntityId!); - const dispatch = useResolverDispatch(); - const nodesHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ panelView: 'nodes' }) - ); - const nodesLinkNavProps = useNavigateOrReplace({ - search: nodesHref, - }); - + event: SafeResolverEvent; /** - * If we don't have the related events for the parent yet, use this effect - * to request them. + * Event type to use in the breadcrumbs */ - useEffect(() => { - if ( - typeof relatedsReady === 'undefined' && - processEntityId !== null && - processEntityId !== undefined - ) { - dispatch({ - type: 'appDetectedMissingEventData', - payload: processEntityId, - }); + eventType: string; + processEvent: SafeResolverEvent; +}) { + const formattedDate = useMemo(() => { + const timestamp = eventModel.timestampSafeVersion(event); + if (timestamp !== undefined) { + return formatDate(new Date(timestamp)); } - }, [relatedsReady, dispatch, processEntityId]); - - const [ - relatedEventToShowDetailsFor, - countBySameCategory, - relatedEventCategory = naString, - sections, - formattedDate, - ] = useSelector((state: ResolverState) => - selectors.relatedEventDisplayInfoByEntityAndSelfId(state)(nodeID, eventID) - ); - - const { subject = '', descriptor = '' } = relatedEventToShowDetailsFor - ? event.descriptiveName(relatedEventToShowDetailsFor) - : {}; - - const nodeDetailHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'nodeDetail', - panelParameters: { nodeID: processEntityId }, - }) - ); - const nodeDetailLinkNavProps = useNavigateOrReplace({ - search: nodeDetailHref, - }); - - const nodeEventsHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'nodeEvents', - panelParameters: { nodeID: processEntityId }, - }) - ); - const nodeEventsLinkNavProps = useNavigateOrReplace({ - search: nodeEventsHref, - }); - - const nodeEventsOfTypeHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'nodeEventsOfType', - panelParameters: { nodeID: processEntityId, eventType: relatedEventCategory }, - }) - ); - const nodeEventsOfTypeLinkNavProps = useNavigateOrReplace({ - search: nodeEventsOfTypeHref, - }); - const crumbs = useMemo(() => { - return [ - { - text: eventsString, - ...nodesLinkNavProps, - }, - { - text: processName, - ...nodeDetailLinkNavProps, - }, - { - text: ( - <> - - - ), - ...nodeEventsLinkNavProps, - }, - { - text: ( - <> - - - ), - ...nodeEventsOfTypeLinkNavProps, - }, - { - text: relatedEventToShowDetailsFor ? ( - - ) : ( - naString - ), - onClick: () => {}, - }, - ]; - }, [ - processName, - eventsString, - totalCount, - countBySameCategory, - naString, - relatedEventCategory, - relatedEventToShowDetailsFor, - subject, - descriptor, - nodeEventsOfTypeLinkNavProps, - nodeEventsLinkNavProps, - nodeDetailLinkNavProps, - nodesLinkNavProps, - ]); - - if (!relatedsReady) { - return ; - } - - /** - * Could happen if user e.g. loads a URL with a bad crumbEvent - */ - if (!relatedEventToShowDetailsFor) { - const errString = i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.relatedDetail.missing', - { - defaultMessage: 'Related event not found.', - } - ); - return ; - } + }, [event]); return ( - + @@ -288,23 +116,49 @@ export const EventDetail = memo(function ({ - + - {sections.map(({ sectionTitle, entries }, index) => { - const displayEntries = entriesForDisplay(entries); + + + ); +}); + +function EventDetailFields({ event }: { event: SafeResolverEvent }) { + const sections = useMemo(() => { + const returnValue: Array<{ + namespace: React.ReactNode; + descriptions: Array<{ title: React.ReactNode; description: React.ReactNode }>; + }> = []; + for (const [key, value] of Object.entries(event)) { + // ignore these keys + if (key === 'agent' || key === 'ecs' || key === 'process' || key === '@timestamp') { + continue; + } + + const section = { + // Group the fields by their top-level namespace + namespace: {key}, + descriptions: deepObjectEntries(value).map(([path, fieldValue]) => ({ + title: {path.join('.')}, + description: {String(fieldValue)}, + })), + }; + returnValue.push(section); + } + return returnValue; + }, [event]); + return ( + <> + {sections.map(({ namespace, descriptions }, index) => { return ( {index === 0 ? null : } - {sectionTitle} + {namespace} @@ -315,12 +169,136 @@ export const EventDetail = memo(function ({ align="left" titleProps={{ className: 'desc-title' }} compressed - listItems={displayEntries} + listItems={descriptions} /> {index === sections.length - 1 ? null : } ); })} - + + ); +} + +function EventDetailBreadcrumbs({ + nodeID, + nodeName, + event, + breadcrumbEventCategory, +}: { + nodeID: string; + nodeName?: string; + event: SafeResolverEvent; + breadcrumbEventCategory: string; +}) { + const countByCategory = useSelector((state: ResolverState) => + selectors.relatedEventCountByType(state)(nodeID, breadcrumbEventCategory) + ); + const relatedEventCount: number | undefined = useSelector((state: ResolverState) => + selectors.relatedEventTotalCount(state)(nodeID) + ); + const nodesLinkNavProps = useLinkProps({ + panelView: 'nodes', + }); + + const nodeDetailLinkNavProps = useLinkProps({ + panelView: 'nodeDetail', + panelParameters: { nodeID }, + }); + + const nodeEventsLinkNavProps = useLinkProps({ + panelView: 'nodeEvents', + panelParameters: { nodeID }, + }); + + const nodeEventsOfTypeLinkNavProps = useLinkProps({ + panelView: 'nodeEventsOfType', + panelParameters: { nodeID, eventType: breadcrumbEventCategory }, + }); + const breadcrumbs = useMemo(() => { + return [ + { + text: i18n.translate( + 'xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.events', + { + defaultMessage: 'Events', + } + ), + ...nodesLinkNavProps, + }, + { + text: nodeName, + ...nodeDetailLinkNavProps, + }, + { + text: ( + + ), + ...nodeEventsLinkNavProps, + }, + { + text: ( + + ), + ...nodeEventsOfTypeLinkNavProps, + }, + { + text: , + }, + ]; + }, [ + breadcrumbEventCategory, + countByCategory, + event, + nodeDetailLinkNavProps, + nodeEventsLinkNavProps, + nodeName, + relatedEventCount, + nodesLinkNavProps, + nodeEventsOfTypeLinkNavProps, + ]); + return ; +} + +const StyledDescriptionList = memo(styled(EuiDescriptionList)` + &.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title { + max-width: 8em; + overflow-wrap: break-word; + } + &.euiDescriptionList.euiDescriptionList--column dd.euiDescriptionList__description { + max-width: calc(100% - 8.5em); + overflow-wrap: break-word; + } +`); + +// Also prevents horizontal scrollbars on long descriptive names +const StyledDescriptiveName = memo(styled(EuiText)` + padding-right: 1em; + overflow-wrap: break-word; +`); + +const StyledFlexTitle = memo(styled('h3')` + display: flex; + flex-flow: row; + font-size: 1.2em; +`); +const StyledTitleRule = memo(styled('hr')` + &.euiHorizontalRule.euiHorizontalRule--full.euiHorizontalRule--marginSmall.override { + display: block; + flex: 1; + margin-left: 0.5em; + } +`); + +const TitleHr = memo(() => { + return ( + ); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx index da5cb1acfed6d..df9cbe9ced541 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx @@ -11,16 +11,13 @@ import { useSelector } from 'react-redux'; import * as selectors from '../../store/selectors'; import { NodeEventsOfType } from './node_events_of_type'; import { NodeEvents } from './node_events'; -import { NodeDetail } from './node_details'; +import { NodeDetail } from './node_detail'; import { NodeList } from './node_list'; import { EventDetail } from './event_detail'; import { PanelViewAndParameters } from '../../types'; /** - * - * This component implements the strategy laid out above by determining the "right" view and doing some other housekeeping e.g. effects to keep the UI-selected node in line with what's indicated by the URL parameters. - * - * @returns {JSX.Element} The "right" table content to show based on the query params as described above + * Show the panel that matches the `panelViewAndParameters` (derived from the browser's location.search) */ export const PanelRouter = memo(function () { const params: PanelViewAndParameters = useSelector(selectors.panelViewAndParameters); @@ -40,6 +37,7 @@ export const PanelRouter = memo(function () { ); } else { diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_details.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx similarity index 74% rename from x-pack/plugins/security_solution/public/resolver/view/panels/node_details.tsx rename to x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx index 48d5089eb5641..04e9de61f6256 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_details.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx @@ -15,23 +15,17 @@ import styled from 'styled-components'; import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list'; import { StyledDescriptionList, StyledTitle } from './styles'; import * as selectors from '../../store/selectors'; -import * as event from '../../../../common/endpoint/models/event'; -import { formatDate, StyledBreadcrumbs, GeneratedText } from './panel_content_utilities'; -import { - processPath, - processPid, - userInfoForProcess, - processParentPid, - md5HashForProcess, - argsForProcess, -} from '../../models/process_event'; +import * as eventModel from '../../../../common/endpoint/models/event'; +import { formatDate, GeneratedText } from './panel_content_utilities'; +import { Breadcrumbs } from './breadcrumbs'; +import { processPath, processPID } from '../../models/process_event'; import { CubeForProcess } from './cube_for_process'; -import { ResolverEvent } from '../../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { useCubeAssets } from '../use_cube_assets'; import { ResolverState } from '../../types'; import { PanelLoading } from './panel_loading'; import { StyledPanel } from '../styles'; -import { useNavigateOrReplace } from '../use_navigate_or_replace'; +import { useLinkProps } from '../use_link_props'; const StyledCubeForProcess = styled(CubeForProcess)` position: relative; @@ -44,7 +38,11 @@ export const NodeDetail = memo(function ({ nodeID }: { nodeID: string }) { ); return ( - {processEvent === null ? : } + {processEvent === null ? ( + + ) : ( + + )} ); }); @@ -53,21 +51,22 @@ export const NodeDetail = memo(function ({ nodeID }: { nodeID: string }) { * A description list view of all the Metadata that goes with a particular process event, like: * Created, PID, User/Domain, etc. */ -const NodeDetailView = memo(function NodeDetailView({ +const NodeDetailView = memo(function ({ processEvent, + nodeID, }: { - processEvent: ResolverEvent; + processEvent: SafeResolverEvent; + nodeID: string; }) { - const processName = event.eventName(processEvent); - const entityId = event.entityId(processEvent); + const processName = eventModel.processNameSafeVersion(processEvent); const isProcessTerminated = useSelector((state: ResolverState) => - selectors.isProcessTerminated(state)(entityId) + selectors.isProcessTerminated(state)(nodeID) ); const relatedEventTotal = useSelector((state: ResolverState) => { - return selectors.relatedEventAggregateTotalByEntityId(state)(entityId); + return selectors.relatedEventTotalCount(state)(nodeID); }); const processInfoEntry: EuiDescriptionListProps['listItems'] = useMemo(() => { - const eventTime = event.eventTimestamp(processEvent); + const eventTime = eventModel.eventTimestamp(processEvent); const dateTime = eventTime === undefined ? null : formatDate(eventTime); const createdEntry = { @@ -82,32 +81,32 @@ const NodeDetailView = memo(function NodeDetailView({ const pidEntry = { title: 'process.pid', - description: processPid(processEvent), + description: processPID(processEvent), }; const userEntry = { title: 'user.name', - description: userInfoForProcess(processEvent)?.name, + description: eventModel.userName(processEvent), }; const domainEntry = { title: 'user.domain', - description: userInfoForProcess(processEvent)?.domain, + description: eventModel.userDomain(processEvent), }; const parentPidEntry = { title: 'process.parent.pid', - description: processParentPid(processEvent), + description: eventModel.parentPID(processEvent), }; const md5Entry = { title: 'process.hash.md5', - description: md5HashForProcess(processEvent), + description: eventModel.md5HashForProcess(processEvent), }; const commandLineEntry = { title: 'process.args', - description: argsForProcess(processEvent), + description: eventModel.argsForProcess(processEvent), }; // This is the data in {title, description} form for the EuiDescriptionList to display @@ -134,12 +133,8 @@ const NodeDetailView = memo(function NodeDetailView({ return processDescriptionListData; }, [processEvent]); - const nodesHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ panelView: 'nodes' }) - ); - - const nodesLinkNavProps = useNavigateOrReplace({ - search: nodesHref, + const nodesLinkNavProps = useLinkProps({ + panelView: 'nodes', }); const crumbs = useMemo(() => { @@ -162,27 +157,20 @@ const NodeDetailView = memo(function NodeDetailView({ defaultMessage="Details for: {processName}" /> ), - onClick: () => {}, }, ]; }, [processName, nodesLinkNavProps]); const { descriptionText } = useCubeAssets(isProcessTerminated, false); - const nodeDetailHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'nodeEvents', - panelParameters: { nodeID: entityId }, - }) - ); - - const nodeDetailNavProps = useNavigateOrReplace({ - search: nodeDetailHref!, + const nodeDetailNavProps = useLinkProps({ + panelView: 'nodeEvents', + panelParameters: { nodeID }, }); const titleID = useMemo(() => htmlIdGenerator('resolverTable')(), []); return ( <> - + @@ -201,7 +189,7 @@ const NodeDetailView = memo(function NodeDetailView({ - + @@ -26,11 +28,21 @@ export function NodeEvents({ nodeID }: { nodeID: string }) { selectors.relatedEventsStats(state)(nodeID) ); if (processEvent === null || relatedEventsStats === undefined) { - return ; + return ( + + + + ); } else { return ( - + + + ); } @@ -47,120 +59,29 @@ export function NodeEvents({ nodeID }: { nodeID: string }) { * | 2 | Network | * */ -const EventCountsForProcess = memo(function EventCountsForProcess({ - processEvent, +const EventCategoryLinks = memo(function ({ + nodeID, relatedStats, }: { - processEvent: ResolverEvent; + nodeID: string; relatedStats: ResolverNodeStats; }) { interface EventCountsTableView { - name: string; + eventType: string; count: number; } - const relatedEventsState = { stats: relatedStats.events.byCategory }; - const processName = processEvent && event.eventName(processEvent); - const processEntityId = event.entityId(processEvent); - /** - * totalCount: This will reflect the aggregated total by category for all related events - * e.g. [dns,file],[dns,file],[registry] will have an aggregate total of 5. This is to keep the - * total number consistent with the "broken out" totals we see elsewhere in the app. - * E.g. on the rleated list by type, the above would show as: - * 2 dns - * 2 file - * 1 registry - * So it would be extremely disorienting to show the user a "3" above that as a total. - */ - const totalCount = Object.values(relatedStats.events.byCategory).reduce( - (sum, val) => sum + val, - 0 - ); - const eventsString = i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.processEventCounts.events', - { - defaultMessage: 'Events', - } - ); - const eventsHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ panelView: 'nodes' }) - ); - - const eventLinkNavProps = useNavigateOrReplace({ - search: eventsHref, - }); - - const processDetailHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'nodeDetail', - panelParameters: { nodeID: processEntityId }, - }) - ); - - const processDetailNavProps = useNavigateOrReplace({ - search: processDetailHref, - }); - - const nodeDetailHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'nodeEvents', - panelParameters: { nodeID: processEntityId }, - }) - ); - - const nodeDetailNavProps = useNavigateOrReplace({ - search: nodeDetailHref!, - }); - const crumbs = useMemo(() => { - return [ - { - text: eventsString, - ...eventLinkNavProps, - }, - { - text: processName, - ...processDetailNavProps, - }, - { - text: ( - - ), - ...nodeDetailNavProps, - }, - ]; - }, [ - processName, - totalCount, - eventsString, - eventLinkNavProps, - nodeDetailNavProps, - processDetailNavProps, - ]); const rows = useMemo(() => { - return Object.entries(relatedEventsState.stats).map( + return Object.entries(relatedStats.events.byCategory).map( ([eventType, count]): EventCountsTableView => { return { - name: eventType, + eventType, count, }; } ); - }, [relatedEventsState]); - - const eventDetailHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'eventDetail', - panelParameters: { nodeID: processEntityId, eventType: name, eventID: processEntityId }, - }) - ); + }, [relatedStats.events.byCategory]); - const eventDetailNavProps = useNavigateOrReplace({ - search: eventDetailHref, - }); const columns = useMemo>>( () => [ { @@ -168,29 +89,100 @@ const EventCountsForProcess = memo(function EventCountsForProcess({ name: i18n.translate('xpack.securitySolution.endpoint.resolver.panel.table.row.count', { defaultMessage: 'Count', }), + 'data-test-subj': 'resolver:panel:node-events:event-type-count', width: '20%', sortable: true, }, { - field: 'name', + field: 'eventType', name: i18n.translate('xpack.securitySolution.endpoint.resolver.panel.table.row.eventType', { defaultMessage: 'Event Type', }), width: '80%', sortable: true, - render(name: string) { - return {name}; + render(eventType: string) { + return ( + + {eventType} + + ); }, }, ], - [eventDetailNavProps] + [nodeID] ); + return items={rows} columns={columns} sorting />; +}); + +const NodeEventsBreadcrumbs = memo(function ({ + nodeID, + nodeName, + totalEventCount, +}: { + nodeID: string; + nodeName: React.ReactNode; + totalEventCount: number; +}) { return ( - <> - - - items={rows} columns={columns} sorting /> - + + ), + ...useLinkProps({ + panelView: 'nodeEvents', + panelParameters: { nodeID }, + }), + }, + ]} + /> ); }); -EventCountsForProcess.displayName = 'EventCountsForProcess'; + +const NodeEventsLink = memo( + ({ + nodeID, + eventType, + children, + }: { + nodeID: string; + eventType: string; + children: React.ReactNode; + }) => { + const props = useLinkProps({ + panelView: 'nodeEventsOfType', + panelParameters: { + nodeID, + eventType, + }, + }); + return ( + + {children} + + ); + } +); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx index afff8d4b75c15..281794ac24d24 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx @@ -4,297 +4,225 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable react/display-name */ - -import React, { memo, useMemo, useEffect, Fragment } from 'react'; +import React, { memo, useCallback, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiText, EuiButtonEmpty, EuiHorizontalRule } from '@elastic/eui'; import { useSelector } from 'react-redux'; import { FormattedMessage } from 'react-intl'; -import styled from 'styled-components'; import { StyledPanel } from '../styles'; -import { formatDate, StyledBreadcrumbs, BoldCode, StyledTime } from './panel_content_utilities'; -import * as event from '../../../../common/endpoint/models/event'; -import { ResolverEvent, ResolverNodeStats } from '../../../../common/endpoint/types'; +import { formatDate, BoldCode, StyledTime } from './panel_content_utilities'; +import { Breadcrumbs } from './breadcrumbs'; +import * as eventModel from '../../../../common/endpoint/models/event'; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; import * as selectors from '../../store/selectors'; -import { useResolverDispatch } from '../use_resolver_dispatch'; -import { RelatedEventLimitWarning } from '../limit_warnings'; import { ResolverState } from '../../types'; -import { useNavigateOrReplace } from '../use_navigate_or_replace'; -import { useRelatedEventDetailNavigation } from '../use_related_event_detail_navigation'; import { PanelLoading } from './panel_loading'; +import { DescriptiveName } from './descriptive_name'; +import { useLinkProps } from '../use_link_props'; /** - * This view presents a list of related events of a given type for a given process. - * It will appear like: - * - * | | - * | :----------------------------------------------------- | - * | **registry deletion** @ *3:32PM..* *HKLM/software...* | - * | **file creation** @ *3:34PM..* *C:/directory/file.exe* | + * Render a list of events that are related to `nodeID` and that have a category of `eventType`. */ - -interface MatchingEventEntry { - formattedDate: string; - eventType: string; - eventCategory: string; - name: { subject: string; descriptor?: string }; - setQueryParams: () => void; -} - -const StyledRelatedLimitWarning = styled(RelatedEventLimitWarning)` - flex-flow: row wrap; - display: block; - align-items: baseline; - margin-top: 1em; - - & .euiCallOutHeader { - display: inline; - margin-right: 0.25em; - } - - & .euiText { - display: inline; - } - - & .euiText p { - display: inline; - } -`; - -const NodeCategoryEntries = memo(function ({ - crumbs, - matchingEventEntries, +export const NodeEventsOfType = memo(function NodeEventsOfType({ + nodeID, eventType, - processEntityId, }: { - crumbs: Array<{ - text: string | JSX.Element | null; - onClick: (event: React.MouseEvent) => void; - href?: string; - }>; - matchingEventEntries: MatchingEventEntry[]; + nodeID: string; eventType: string; - processEntityId: string; }) { - const relatedLookupsByCategory = useSelector(selectors.relatedEventInfoByEntityId); - const lookupsForThisNode = relatedLookupsByCategory(processEntityId); - const shouldShowLimitWarning = lookupsForThisNode?.shouldShowLimitForCategory(eventType); - const numberDisplayed = lookupsForThisNode?.numberActuallyDisplayedForCategory(eventType); - const numberMissing = lookupsForThisNode?.numberNotDisplayedForCategory(eventType); - - return ( - <> - - {shouldShowLimitWarning && typeof numberDisplayed !== 'undefined' && numberMissing ? ( - - ) : null} - - <> - {matchingEventEntries.map((eventView, index) => { - const { subject, descriptor = '' } = eventView.name; - return ( - - - - - - - - - - - - - - {index === matchingEventEntries.length - 1 ? null : } - - ); - })} - - - ); -}); - -export function NodeEventsOfType({ nodeID, eventType }: { nodeID: string; eventType: string }) { const processEvent = useSelector((state: ResolverState) => selectors.processEventForID(state)(nodeID) ); - const relatedEventsStats = useSelector((state: ResolverState) => - selectors.relatedEventsStats(state)(nodeID) + const eventCount = useSelector( + (state: ResolverState) => selectors.relatedEventsStats(state)(nodeID)?.events.total + ); + const eventsInCategoryCount = useSelector( + (state: ResolverState) => + selectors.relatedEventsStats(state)(nodeID)?.events.byCategory[eventType] + ); + const events = useSelector( + useCallback( + (state: ResolverState) => { + return selectors.relatedEventsByCategory(state)(nodeID, eventType); + }, + [eventType, nodeID] + ) ); return ( - + {eventCount === undefined || processEvent === null ? ( + + ) : ( + <> + + + + + )} ); -} +}); -const NodeEventList = memo(function ({ - processEvent, +/** + * Rendered for each event in the list. + */ +const NodeEventsListItem = memo(function ({ + event, + nodeID, eventType, - relatedStats, }: { - processEvent: ResolverEvent | null; + event: SafeResolverEvent; + nodeID: string; eventType: string; - relatedStats: ResolverNodeStats | undefined; }) { - const processName = processEvent && event.eventName(processEvent); - const processEntityId = processEvent ? event.entityId(processEvent) : ''; - const nodesHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ panelView: 'nodes' }) - ); - - const nodesLinkNavProps = useNavigateOrReplace({ - search: nodesHref, + const timestamp = eventModel.eventTimestamp(event); + const date = timestamp !== undefined ? formatDate(timestamp) : timestamp; + const linkProps = useLinkProps({ + panelView: 'eventDetail', + panelParameters: { + nodeID, + eventType, + eventID: String(eventModel.eventID(event)), + }, }); - const totalCount = relatedStats - ? Object.values(relatedStats.events.byCategory).reduce((sum, val) => sum + val, 0) - : 0; - const eventsString = i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.processEventListByType.events', - { - defaultMessage: 'Events', - } + return ( + <> + + + + + + + + + + + + + ); +}); - const relatedsReadyMap = useSelector(selectors.relatedEventsReady); - const relatedsReady = processEntityId && relatedsReadyMap.get(processEntityId); - - const dispatch = useResolverDispatch(); - - useEffect(() => { - if (typeof relatedsReady === 'undefined') { - dispatch({ - type: 'appDetectedMissingEventData', - payload: processEntityId, - }); - } - }, [relatedsReady, dispatch, processEntityId]); - - const relatedByCategory = useSelector(selectors.relatedEventsByCategory); - const eventsForCurrentCategory = relatedByCategory(processEntityId)(eventType); - const relatedEventDetailNavigation = useRelatedEventDetailNavigation({ - nodeID: processEntityId, - category: eventType, - events: eventsForCurrentCategory, - }); - +/** + * Renders a list of events with a separator in between. + */ +const NodeEventList = memo(function NodeEventList({ + eventType, + events, + nodeID, +}: { + eventType: string; /** - * A list entry will be displayed for each of these + * The events to list. */ - const matchingEventEntries: MatchingEventEntry[] = useMemo(() => { - return eventsForCurrentCategory.map((resolverEvent) => { - const eventTime = event.eventTimestamp(resolverEvent); - const formattedDate = typeof eventTime === 'undefined' ? '' : formatDate(eventTime); - const entityId = event.eventId(resolverEvent); - return { - formattedDate, - eventCategory: `${eventType}`, - eventType: `${event.ecsEventType(resolverEvent)}`, - name: event.descriptiveName(resolverEvent), - setQueryParams: () => relatedEventDetailNavigation(entityId), - }; - }); - }, [eventType, eventsForCurrentCategory, relatedEventDetailNavigation]); - - const nodeDetailHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'nodeDetail', - panelParameters: { nodeID: processEntityId }, - }) + events: SafeResolverEvent[]; + nodeID: string; +}) { + return ( + <> + {events.map((event, index) => ( + + + {index === events.length - 1 ? null : } + + ))} + ); +}); - const nodeDetailNavProps = useNavigateOrReplace({ - search: nodeDetailHref, +/** + * Renders `Breadcrumbs`. + */ +const NodeEventsOfTypeBreadcrumbs = memo(function ({ + nodeName, + eventType, + eventCount, + nodeID, + /** + * The count of events in the category that this list is showing. + */ + eventsInCategoryCount, +}: { + nodeName: React.ReactNode; + eventType: string; + /** + * The events to list. + */ + eventCount: number; + nodeID: string; + /** + * The count of events in the category that this list is showing. + */ + eventsInCategoryCount: number | undefined; +}) { + const nodesLinkNavProps = useLinkProps({ + panelView: 'nodes', }); - const nodeEventsHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'nodeEvents', - panelParameters: { nodeID: processEntityId }, - }) - ); - - const nodeEventsNavProps = useNavigateOrReplace({ - search: nodeEventsHref, + const nodeDetailNavProps = useLinkProps({ + panelView: 'nodeDetail', + panelParameters: { nodeID }, }); - const crumbs = useMemo(() => { - return [ - { - text: eventsString, - ...nodesLinkNavProps, - }, - { - text: processName, - ...nodeDetailNavProps, - }, - { - text: ( - - ), - ...nodeEventsNavProps, - }, - { - text: ( - - ), - onClick: () => {}, - }, - ]; - }, [ - eventType, - eventsString, - matchingEventEntries.length, - processName, - totalCount, - nodeDetailNavProps, - nodesLinkNavProps, - nodeEventsNavProps, - ]); - if (!relatedsReady) { - return ; - } + const nodeEventsNavProps = useLinkProps({ + panelView: 'nodeEvents', + panelParameters: { nodeID }, + }); return ( - + ), + ...nodeEventsNavProps, + }, + { + text: ( + + ), + }, + ]} /> ); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx index 6113cea4c4edc..8fc6e7cc66c79 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx @@ -4,9 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable @elastic/eui/href-or-on-click */ + +/* eslint-disable no-duplicate-imports */ + +import { useDispatch } from 'react-redux'; + /* eslint-disable react/display-name */ -import React, { memo, useMemo } from 'react'; +import React, { memo, useMemo, useCallback, useContext } from 'react'; import { EuiBasicTableColumn, EuiBadge, @@ -16,71 +22,31 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useSelector } from 'react-redux'; -import styled from 'styled-components'; +import { SideEffectContext } from '../side_effect_context'; import { StyledPanel } from '../styles'; -import * as event from '../../../../common/endpoint/models/event'; +import { + StyledLabelTitle, + StyledAnalyzedEvent, + StyledLabelContainer, + StyledButtonTextContainer, +} from './styles'; +import * as eventModel from '../../../../common/endpoint/models/event'; import * as selectors from '../../store/selectors'; -import { formatter, StyledBreadcrumbs } from './panel_content_utilities'; +import { formatter } from './panel_content_utilities'; +import { Breadcrumbs } from './breadcrumbs'; import { CubeForProcess } from './cube_for_process'; -import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { LimitWarning } from '../limit_warnings'; import { ResolverState } from '../../types'; -import { useNavigateOrReplace } from '../use_navigate_or_replace'; +import { useLinkProps } from '../use_link_props'; import { useColors } from '../use_colors'; - -const StyledLimitWarning = styled(LimitWarning)` - flex-flow: row wrap; - display: block; - align-items: baseline; - margin-top: 1em; - - & .euiCallOutHeader { - display: inline; - margin-right: 0.25em; - } - - & .euiText { - display: inline; - } - - & .euiText p { - display: inline; - } -`; - -const StyledButtonTextContainer = styled.div` - align-items: center; - display: flex; - flex-direction: row; -`; - -const StyledAnalyzedEvent = styled.div` - color: ${(props) => props.color}; - font-size: 10.5px; - font-weight: 700; -`; - -const StyledLabelTitle = styled.div``; - -const StyledLabelContainer = styled.div` - display: inline-block; - flex: 3; - min-width: 0; - - ${StyledAnalyzedEvent}, - ${StyledLabelTitle} { - overflow: hidden; - text-align: left; - text-overflow: ellipsis; - white-space: nowrap; - } -`; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; +import { ResolverAction } from '../../store/actions'; interface ProcessTableView { name?: string; timestamp?: Date; + nodeID: string; event: SafeResolverEvent; - href: string | undefined; } /** @@ -99,8 +65,8 @@ export const NodeList = memo(() => { ), sortable: true, truncateText: true, - render(name: string, item: ProcessTableView) { - return ; + render(name: string | undefined, item: ProcessTableView) { + return ; }, }, { @@ -132,42 +98,26 @@ export const NodeList = memo(() => { [] ); - const { processNodePositions } = useSelector(selectors.layout); - const nodeHrefs: Map = useSelector( - (state: ResolverState) => { - const relativeHref = selectors.relativeHref(state); - return new Map( - [...processNodePositions.keys()].map((processEvent) => { - const nodeID = event.entityIDSafeVersion(processEvent); - if (nodeID === undefined) { - return [processEvent, null]; - } - return [ - processEvent, - relativeHref({ - panelView: 'nodeDetail', - panelParameters: { - nodeID, - }, - }), - ]; - }) - ); - } - ); - const processTableView: ProcessTableView[] = useMemo( - () => - [...processNodePositions.keys()].map((processEvent) => { - const name = event.processNameSafeVersion(processEvent); - return { - name, - timestamp: event.timestampAsDateSafeVersion(processEvent), - event: processEvent, - href: nodeHrefs.get(processEvent) ?? undefined, - }; - }), - [processNodePositions, nodeHrefs] + const processTableView: ProcessTableView[] = useSelector( + useCallback((state: ResolverState) => { + const { processNodePositions } = selectors.layout(state); + const view: ProcessTableView[] = []; + for (const processEvent of processNodePositions.keys()) { + const name = eventModel.processNameSafeVersion(processEvent); + const nodeID = eventModel.entityIDSafeVersion(processEvent); + if (nodeID !== undefined) { + view.push({ + name, + timestamp: eventModel.timestampAsDateSafeVersion(processEvent), + nodeID, + event: processEvent, + }); + } + } + return view; + }, []) ); + const numberOfProcesses = processTableView.length; const crumbs = useMemo(() => { @@ -176,7 +126,6 @@ export const NodeList = memo(() => { text: i18n.translate('xpack.securitySolution.resolver.panel.nodeList.title', { defaultMessage: 'All Process Events', }), - onClick: () => {}, }, ]; }, []); @@ -187,8 +136,8 @@ export const NodeList = memo(() => { const rowProps = useMemo(() => ({ 'data-test-subj': 'resolver:node-list:item' }), []); return ( - - {showWarning && } + + {showWarning && } rowProps={rowProps} @@ -201,16 +150,40 @@ export const NodeList = memo(() => { ); }); -function NodeDetailLink({ name, item }: { name: string; item: ProcessTableView }) { - const entityID = event.entityIDSafeVersion(item.event); - const originID = useSelector(selectors.originID); - const isOrigin = originID === entityID; +function NodeDetailLink({ + name, + nodeID, + event, +}: { + name?: string; + nodeID: string; + event: SafeResolverEvent; +}) { + const isOrigin = useSelector((state: ResolverState) => { + return selectors.originID(state) === nodeID; + }); const isTerminated = useSelector((state: ResolverState) => - entityID === undefined ? false : selectors.isProcessTerminated(state)(entityID) + nodeID === undefined ? false : selectors.isProcessTerminated(state)(nodeID) ); const { descriptionText } = useColors(); + const linkProps = useLinkProps({ panelView: 'nodeDetail', panelParameters: { nodeID } }); + const dispatch: (action: ResolverAction) => void = useDispatch(); + const { timestamp } = useContext(SideEffectContext); + const handleOnClick = useCallback( + (mouseEvent: React.MouseEvent) => { + linkProps.onClick(mouseEvent); + dispatch({ + type: 'userBroughtProcessIntoView', + payload: { + process: event, + time: timestamp(), + }, + }); + }, + [timestamp, linkProps, dispatch, event] + ); return ( - + {name === '' ? ( {i18n.translate( diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_error.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_error.tsx index 3b10a8db2bf12..199758145f117 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_error.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_error.tsx @@ -7,11 +7,8 @@ import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiText, EuiButtonEmpty } from '@elastic/eui'; import React, { memo, useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { useNavigateOrReplace } from '../use_navigate_or_replace'; -import * as selectors from '../../store/selectors'; -import { ResolverState } from '../../types'; -import { StyledBreadcrumbs } from './panel_content_utilities'; +import { Breadcrumbs } from './breadcrumbs'; +import { useLinkProps } from '../use_link_props'; /** * Display an error in the panel when something goes wrong and give the user a way to "retreat" back to a default state. @@ -24,12 +21,10 @@ export const PanelContentError = memo(function ({ }: { translatedErrorMessage: string; }) { - const nodesHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ panelView: 'nodes' }) - ); - const nodesLinkNavProps = useNavigateOrReplace({ - search: nodesHref, + const nodesLinkNavProps = useLinkProps({ + panelView: 'nodes', }); + const crumbs = useMemo(() => { return [ { @@ -42,13 +37,12 @@ export const PanelContentError = memo(function ({ text: i18n.translate('xpack.securitySolution.endpoint.resolver.panel.error.error', { defaultMessage: 'Error', }), - onClick: () => {}, }, ]; }, [nodesLinkNavProps]); return ( <> - + {translatedErrorMessage} @@ -60,4 +54,3 @@ export const PanelContentError = memo(function ({ ); }); -PanelContentError.displayName = 'TableServiceError'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx index a7d76277c6ab1..5ca34b33b2396 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx @@ -7,10 +7,9 @@ /* eslint-disable react/display-name */ import { i18n } from '@kbn/i18n'; -import { EuiBreadcrumbs, EuiCode, EuiBetaBadge } from '@elastic/eui'; +import { EuiCode } from '@elastic/eui'; import styled from 'styled-components'; import React, { memo } from 'react'; -import { useColors } from '../use_colors'; /** * A bold version of EuiCode to display certain titles with @@ -21,30 +20,6 @@ export const BoldCode = styled(EuiCode)` } `; -const BetaHeader = styled(`header`)` - margin-bottom: 1em; -`; - -const ThemedBreadcrumbs = styled(EuiBreadcrumbs)<{ background: string; text: string }>` - &.euiBreadcrumbs { - background-color: ${(props) => props.background}; - color: ${(props) => props.text}; - padding: 1em; - border-radius: 5px; - } - - & .euiBreadcrumbSeparator { - background: ${(props) => props.text}; - } -`; - -const betaBadgeLabel = i18n.translate( - 'xpack.securitySolution.enpdoint.resolver.panelutils.betaBadgeLabel', - { - defaultMessage: 'BETA', - } -); - /** * A component that renders an element with breaking opportunities (``s) * spliced into text children at word boundaries. @@ -85,31 +60,6 @@ export const StyledTime = memo(styled('time')` text-align: start; `); -type Breadcrumbs = Parameters[0]['breadcrumbs']; -/** - * Breadcrumb menu with adjustments per direction from UX team - */ -export const StyledBreadcrumbs = memo(function StyledBreadcrumbs({ - breadcrumbs, -}: { - breadcrumbs: Breadcrumbs; -}) { - const { resolverBreadcrumbBackground, resolverEdgeText } = useColors(); - return ( - <> - - - - - - ); -}); - /** * Long formatter (to second) for DateTime */ @@ -122,12 +72,6 @@ export const formatter = new Intl.DateTimeFormat(i18n.getLocale(), { second: '2-digit', }); -const invalidDateText = i18n.translate( - 'xpack.securitySolution.enpdoint.resolver.panelutils.invaliddate', - { - defaultMessage: 'Invalid Date', - } -); /** * @returns {string} A nicely formatted string for a date */ @@ -140,6 +84,8 @@ export function formatDate( if (isFinite(date.getTime())) { return formatter.format(date); } else { - return invalidDateText; + return i18n.translate('xpack.securitySolution.enpdoint.resolver.panelutils.invaliddate', { + defaultMessage: 'Invalid Date', + }); } } diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_loading.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_loading.tsx index 864990e4d96ab..2de0bf5d320ea 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_loading.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_loading.tsx @@ -5,13 +5,10 @@ */ import React, { useMemo } from 'react'; -import { useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiTitle } from '@elastic/eui'; -import * as selectors from '../../store/selectors'; -import { StyledBreadcrumbs } from './panel_content_utilities'; -import { useNavigateOrReplace } from '../use_navigate_or_replace'; -import { ResolverState } from '../../types'; +import { Breadcrumbs } from './breadcrumbs'; +import { useLinkProps } from '../use_link_props'; export function PanelLoading() { const waitingString = i18n.translate( @@ -26,11 +23,8 @@ export function PanelLoading() { defaultMessage: 'Events', } ); - const nodesHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ panelView: 'nodes' }) - ); - const nodesLinkNavProps = useNavigateOrReplace({ - search: nodesHref, + const nodesLinkNavProps = useLinkProps({ + panelView: 'nodes', }); const waitCrumbs = useMemo(() => { return [ @@ -42,7 +36,7 @@ export function PanelLoading() { }, [nodesLinkNavProps, eventsString]); return ( <> - +

{waitingString}

diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/styles.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/styles.tsx index c5d5ae53a5580..09a25ac125a2a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/styles.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/styles.tsx @@ -3,6 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +/* eslint-disable no-duplicate-imports */ + +import { EuiBreadcrumbs } from '@elastic/eui'; + import styled from 'styled-components'; import { EuiDescriptionList } from '@elastic/eui'; @@ -15,3 +20,48 @@ export const StyledDescriptionList = styled(EuiDescriptionList)` export const StyledTitle = styled('h4')` overflow-wrap: break-word; `; + +export const BetaHeader = styled(`header`)` + margin-bottom: 1em; +`; + +export const ThemedBreadcrumbs = styled(EuiBreadcrumbs)<{ background: string; text: string }>` + &.euiBreadcrumbs { + background-color: ${(props) => props.background}; + color: ${(props) => props.text}; + padding: 1em; + border-radius: 5px; + } + + & .euiBreadcrumbSeparator { + background: ${(props) => props.text}; + } +`; + +export const StyledButtonTextContainer = styled.div` + align-items: center; + display: flex; + flex-direction: row; +`; + +export const StyledAnalyzedEvent = styled.div` + color: ${(props) => props.color}; + font-size: 10.5px; + font-weight: 700; +`; + +export const StyledLabelTitle = styled.div``; + +export const StyledLabelContainer = styled.div` + display: inline-block; + flex: 3; + min-width: 0; + + ${StyledAnalyzedEvent}, + ${StyledLabelTitle} { + overflow: hidden; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; + } +`; diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 65ec395080f86..4d647760edb9c 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -12,15 +12,15 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { NodeSubMenu } from './submenu'; import { applyMatrix3 } from '../models/vector2'; import { Vector2, Matrix3, ResolverState } from '../types'; -import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../common/endpoint/types'; import { useResolverDispatch } from './use_resolver_dispatch'; import * as eventModel from '../../../common/endpoint/models/event'; import * as selectors from '../store/selectors'; -import { useNavigateOrReplace } from './use_navigate_or_replace'; import { fontSize } from './font_size'; import { useCubeAssets } from './use_cube_assets'; import { useSymbolIDs } from './use_symbol_ids'; import { useColors } from './use_colors'; +import { useLinkProps } from './use_link_props'; interface StyledActionsContainer { readonly color: string; @@ -192,7 +192,6 @@ const UnstyledProcessEventDot = React.memo( /** * Type in non-SVG components scales as follows: - * (These values were adjusted to match the proportions in the comps provided by UX/Design) * 18.75 : The smallest readable font size at which labels/descriptions can be read. Font size will not scale below this. * 12.5 : A 'slope' at which the font size will scale w.r.t. to zoom level otherwise */ @@ -239,15 +238,10 @@ const UnstyledProcessEventDot = React.memo( const isOrigin = nodeID === originID; const dispatch = useResolverDispatch(); - const processDetailHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'nodeDetail', - panelParameters: { nodeID }, - }) - ); - const processDetailNavProps = useNavigateOrReplace({ - search: processDetailHref, + const processDetailNavProps = useLinkProps({ + panelView: 'nodeDetail', + panelParameters: { nodeID }, }); const handleFocus = useCallback(() => { @@ -272,7 +266,7 @@ const UnstyledProcessEventDot = React.memo( ); const grandTotal: number | null = useSelector((state: ResolverState) => - selectors.relatedEventTotalForProcess(state)(event as ResolverEvent) + selectors.relatedEventTotalForProcess(state)(event) ); /* eslint-disable jsx-a11y/click-events-have-key-events */ @@ -376,12 +370,13 @@ const UnstyledProcessEventDot = React.memo( backgroundColor={colorMap.resolverBackground} color={colorMap.descriptionText} isDisplaying={isShowingDescriptionText} + data-test-subj="resolver:node:description" > diff --git a/x-pack/plugins/security_solution/public/resolver/view/styles.tsx b/x-pack/plugins/security_solution/public/resolver/view/styles.tsx index fb4d4d289d254..7def5d3362d4f 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/styles.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/styles.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel } from '@elastic/eui'; +import { EuiPanel, EuiCallOut } from '@elastic/eui'; import styled from 'styled-components'; @@ -62,3 +62,26 @@ export const GraphContainer = styled.div` flex-grow: 1; contain: layout; `; + +/** + * See `RelatedEventLimitWarning` + */ +export const LimitWarningsEuiCallOut = styled(EuiCallOut)` + flex-flow: row wrap; + display: block; + align-items: baseline; + margin-top: 1em; + + & .euiCallOutHeader { + display: inline; + margin-right: 0.25em; + } + + & .euiText { + display: inline; + } + + & .euiText p { + display: inline; + } +`; diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx index 495cd238d22fc..5406b444cee56 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx @@ -11,7 +11,7 @@ import { useCamera, useAutoUpdatingClientRect } from './use_camera'; import { Provider } from 'react-redux'; import * as selectors from '../store/selectors'; import { Matrix3, ResolverStore, SideEffectSimulator } from '../types'; -import { ResolverEvent } from '../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../common/endpoint/types'; import { SideEffectContext } from './side_effect_context'; import { applyMatrix3 } from '../models/vector2'; import { sideEffectSimulatorFactory } from './side_effect_simulator_factory'; @@ -33,7 +33,7 @@ describe('useCamera on an unpainted element', () => { beforeEach(async () => { store = createStore(resolverReducer); - const Test = function Test() { + const Test = function () { const camera = useCamera(); const { ref, onMouseDown } = camera; projectionMatrix = camera.projectionMatrix; @@ -160,9 +160,9 @@ describe('useCamera on an unpainted element', () => { expect(simulator.mock.requestAnimationFrame).not.toHaveBeenCalled(); }); describe('when the camera begins animation', () => { - let process: ResolverEvent; + let process: SafeResolverEvent; beforeEach(() => { - const events: ResolverEvent[] = []; + const events: SafeResolverEvent[] = []; const numberOfEvents: number = 10; for (let index = 0; index < numberOfEvents; index++) { @@ -190,9 +190,9 @@ describe('useCamera on an unpainted element', () => { } else { throw new Error('failed to create tree'); } - const processes: ResolverEvent[] = [ + const processes: SafeResolverEvent[] = [ ...selectors.layout(store.getState()).processNodePositions.keys(), - ] as ResolverEvent[]; + ]; process = processes[processes.length - 1]; if (!process) { throw new Error('missing the process to bring into view'); diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_link_props.ts b/x-pack/plugins/security_solution/public/resolver/view/use_link_props.ts new file mode 100644 index 0000000000000..5645edec7e1ca --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/use_link_props.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useSelector } from 'react-redux'; +import { MouseEventHandler } from 'react'; +import { useNavigateOrReplace } from './use_navigate_or_replace'; + +import * as selectors from '../store/selectors'; +import { PanelViewAndParameters, ResolverState } from '../types'; + +type EventHandlerCallback = MouseEventHandler; + +/** + * Get an `onClick` function and an `href` string. Use these as props for `` elements. + * `onClick` will use navigate to the `panelViewAndParameters` using `history.push`. + * the `href` points to `panelViewAndParameters`. + * Existing `search` parameters are maintained. + */ +export function useLinkProps( + panelViewAndParameters: PanelViewAndParameters +): { href: string; onClick: EventHandlerCallback } { + const search = useSelector((state: ResolverState) => + selectors.relativeHref(state)(panelViewAndParameters) + ); + + return useNavigateOrReplace({ + search, + }); +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_related_event_by_category_navigation.ts b/x-pack/plugins/security_solution/public/resolver/view/use_related_event_by_category_navigation.ts index f994350132c35..6810837ae031a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_related_event_by_category_navigation.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_related_event_by_category_navigation.ts @@ -12,7 +12,7 @@ import * as selectors from '../store/selectors'; /** * A hook that takes a nodeID and a record of categories, and returns a function that * navigates to the proper url when called with a category. - * @deprecated + * @deprecated See `useLinkProps` */ export function useRelatedEventByCategoryNavigation({ nodeID, diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_related_event_detail_navigation.ts b/x-pack/plugins/security_solution/public/resolver/view/use_related_event_detail_navigation.ts deleted file mode 100644 index 9fc74a7567c47..0000000000000 --- a/x-pack/plugins/security_solution/public/resolver/view/use_related_event_detail_navigation.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; - * you may not use this file except in compliance with the Elastic License. - */ -import { useCallback } from 'react'; -import { useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; -import { ResolverState } from '../types'; -import { ResolverEvent } from '../../../common/endpoint/types'; -import * as selectors from '../store/selectors'; - -/** - * @deprecated - */ -export function useRelatedEventDetailNavigation({ - nodeID, - category, - events, -}: { - nodeID: string; - category: string; - events: ResolverEvent[]; -}) { - const relatedEventDetailUrls = useSelector((state: ResolverState) => - selectors.relatedEventDetailHrefs(state)(category, nodeID, events) - ); - const history = useHistory(); - return useCallback( - (entityID: string | number | undefined) => { - if (entityID !== undefined) { - const urlForEntityID = relatedEventDetailUrls.get(String(entityID)); - if (urlForEntityID !== null && urlForEntityID !== undefined) { - return history.replace({ search: urlForEntityID }); - } - } - }, - [history, relatedEventDetailUrls] - ); -} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts index b796913118c99..5bc911fb075b5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts @@ -9,7 +9,7 @@ import { ILegacyScopedClusterClient } from 'kibana/server'; import { parentEntityIDSafeVersion, entityIDSafeVersion, - getAncestryAsArray, + ancestry, } from '../../../../../common/endpoint/models/event'; import { SafeResolverAncestry, @@ -35,7 +35,8 @@ export class AncestryQueryHandler implements QueryHandler legacyEndpointID: string | undefined, originNode: SafeResolverLifecycleNode | undefined ) { - this.ancestorsToFind = getAncestryAsArray(originNode?.lifecycle[0]).slice(0, levels); + const event = originNode?.lifecycle[0]; + this.ancestorsToFind = (event ? ancestry(event) : []).slice(0, levels); this.query = new LifecycleQuery(indexPattern, legacyEndpointID); // add the origin node to the response if it exists @@ -108,7 +109,7 @@ export class AncestryQueryHandler implements QueryHandler this.levels = this.levels - ancestryNodes.size; // the results come back in ascending order on timestamp so the first entry in the // results should be the further ancestor (most distant grandparent) - this.ancestorsToFind = getAncestryAsArray(results[0]).slice(0, this.levels); + this.ancestorsToFind = ancestry(results[0]).slice(0, this.levels); }; /** diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts index f54472141c1de..1a871891b1ed5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts @@ -4,12 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - parentEntityIDSafeVersion, - isProcessRunning, - getAncestryAsArray, - entityIDSafeVersion, -} from '../../../../../common/endpoint/models/event'; +import * as eventModel from '../../../../../common/endpoint/models/event'; import { SafeResolverChildren, SafeResolverChildNode, @@ -72,7 +67,7 @@ export class ChildrenNodesHelper { */ addLifecycleEvents(lifecycle: SafeResolverEvent[]) { for (const event of lifecycle) { - const entityID = entityIDSafeVersion(event); + const entityID = eventModel.entityIDSafeVersion(event); if (entityID) { const cachedChild = this.getOrCreateChildNode(entityID); cachedChild.lifecycle.push(event); @@ -93,19 +88,19 @@ export class ChildrenNodesHelper { const nonLeafNodes: Set = new Set(); const isDistantGrandchild = (event: ChildEvent) => { - const ancestry = getAncestryAsArray(event); + const ancestry = eventModel.ancestry(event); return ancestry.length > 0 && queriedNodes.has(ancestry[ancestry.length - 1]); }; for (const event of startEvents) { - const parentID = parentEntityIDSafeVersion(event); - const entityID = entityIDSafeVersion(event); - if (parentID && entityID && isProcessRunning(event)) { + const parentID = eventModel.parentEntityIDSafeVersion(event); + const entityID = eventModel.entityIDSafeVersion(event); + if (parentID && entityID && eventModel.isProcessRunning(event)) { // don't actually add the start event to the node, because that'll be done in // a different call const childNode = this.getOrCreateChildNode(entityID); - const ancestry = getAncestryAsArray(event); + const ancestry = eventModel.ancestry(event); // This is to handle the following unlikely but possible scenario: // if an alert was generated by the kernel process (parent process of all other processes) then // the direct children of that process would only have an ancestry array of [parent_kernel], a single value in the array. diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index fe9eaf0615183..680f8933b0373 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -15878,19 +15878,14 @@ "xpack.securitySolution.endpoint.resolver.panel.error.goBack": "このリンクをクリックすると、すべてのプロセスのリストに戻ります。", "xpack.securitySolution.endpoint.resolver.panel.processDescList.events": "イベント", "xpack.securitySolution.endpoint.resolver.panel.processEventCounts.events": "イベント", - "xpack.securitySolution.endpoint.resolver.panel.processEventListByType.eventDescriptiveName": "{descriptor} {subject}", "xpack.securitySolution.endpoint.resolver.panel.processEventListByType.events": "イベント", "xpack.securitySolution.endpoint.resolver.panel.relatedCounts.numberOfEventsInCrumb": "{totalCount}件のイベント", - "xpack.securitySolution.endpoint.resolver.panel.relatedDetail.missing": "関連イベントが見つかりません。", "xpack.securitySolution.endpoint.resolver.panel.relatedDetail.wait": "イベントを待機しています...", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.atTime": "@ {date}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.categoryAndType": "{category} {eventType}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.countByCategory": "{count} {category}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.detailsForProcessName": "詳細:{processName}", - "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.eventDescriptiveName": "{descriptor} {subject}", - "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.eventDescriptiveNameInTitle": "{descriptor} {subject}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.events": "イベント", - "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.NA": "N/A", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.numberOfEvents": "{totalCount}件のイベント", "xpack.securitySolution.endpoint.resolver.panel.relatedEventList.countByCategory": "{count} {category}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventList.numberOfEvents": "{totalCount}件のイベント", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e1ae8c30960c2..6e11dabd9843c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -15888,19 +15888,14 @@ "xpack.securitySolution.endpoint.resolver.panel.error.goBack": "单击此链接以返回到所有进程的列表。", "xpack.securitySolution.endpoint.resolver.panel.processDescList.events": "事件", "xpack.securitySolution.endpoint.resolver.panel.processEventCounts.events": "事件", - "xpack.securitySolution.endpoint.resolver.panel.processEventListByType.eventDescriptiveName": "{descriptor} {subject}", "xpack.securitySolution.endpoint.resolver.panel.processEventListByType.events": "事件", "xpack.securitySolution.endpoint.resolver.panel.relatedCounts.numberOfEventsInCrumb": "{totalCount} 个事件", - "xpack.securitySolution.endpoint.resolver.panel.relatedDetail.missing": "找不到相关事件。", "xpack.securitySolution.endpoint.resolver.panel.relatedDetail.wait": "等候事件......", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.atTime": "@ {date}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.categoryAndType": "{category} {eventType}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.countByCategory": "{count} 个{category}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.detailsForProcessName": "{processName} 的详情", - "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.eventDescriptiveName": "{descriptor} {subject}", - "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.eventDescriptiveNameInTitle": "{descriptor} {subject}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.events": "事件", - "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.NA": "不可用", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.numberOfEvents": "{totalCount} 个事件", "xpack.securitySolution.endpoint.resolver.panel.relatedEventList.countByCategory": "{count} 个{category}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventList.numberOfEvents": "{totalCount} 个事件", diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx index f3d1eb60bf1c0..d70d46fcbc015 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx @@ -61,12 +61,12 @@ const AppRoot = React.memo( storeFactory, ResolverWithoutProviders, mocks: { - dataAccessLayer: { noAncestorsTwoChildren }, + dataAccessLayer: { noAncestorsTwoChildrenWithRelatedEventsOnOrigin }, }, } = resolverPluginSetup; const dataAccessLayer: DataAccessLayer = useMemo( - () => noAncestorsTwoChildren().dataAccessLayer, - [noAncestorsTwoChildren] + () => noAncestorsTwoChildrenWithRelatedEventsOnOrigin().dataAccessLayer, + [noAncestorsTwoChildrenWithRelatedEventsOnOrigin] ); const store = useMemo(() => { From 934d53384cd40a8c9f9ddad16df4ac13db2e0ee5 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Wed, 23 Sep 2020 16:23:36 +0200 Subject: [PATCH 46/92] chore: move advanced_settings and management plugins to App team codeowners (#78285) --- .github/CODEOWNERS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f4e620dea95a9..8a8cc5c5e448c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,10 +7,12 @@ /x-pack/plugins/discover_enhanced/ @elastic/kibana-app /x-pack/plugins/lens/ @elastic/kibana-app /x-pack/plugins/graph/ @elastic/kibana-app +/src/plugins/advanced_settings/ @elastic/kibana-app /src/plugins/charts/ @elastic/kibana-app /src/plugins/dashboard/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app /src/plugins/input_control_vis/ @elastic/kibana-app +/src/plugins/management/ @elastic/kibana-app /src/plugins/kibana_legacy/ @elastic/kibana-app /src/plugins/vis_default_editor/ @elastic/kibana-app /src/plugins/vis_type_markdown/ @elastic/kibana-app @@ -38,7 +40,6 @@ /examples/url_generators_explorer/ @elastic/kibana-app-arch /packages/elastic-datemath/ @elastic/kibana-app-arch /packages/kbn-interpreter/ @elastic/kibana-app-arch -/src/plugins/advanced_settings/ @elastic/kibana-app-arch /src/plugins/bfetch/ @elastic/kibana-app-arch /src/plugins/data/ @elastic/kibana-app-arch /src/plugins/embeddable/ @elastic/kibana-app-arch @@ -47,7 +48,6 @@ /src/plugins/kibana_react/ @elastic/kibana-app-arch /src/plugins/kibana_react/public/code_editor @elastic/kibana-canvas /src/plugins/kibana_utils/ @elastic/kibana-app-arch -/src/plugins/management/ @elastic/kibana-app-arch /src/plugins/navigation/ @elastic/kibana-app-arch /src/plugins/share/ @elastic/kibana-app-arch /src/plugins/ui_actions/ @elastic/kibana-app-arch From 180827cfe1ec346586671ea5e567b11b96470244 Mon Sep 17 00:00:00 2001 From: PavithraCP <31021423+PavithraCP@users.noreply.github.com> Date: Wed, 23 Sep 2020 11:01:05 -0400 Subject: [PATCH 47/92] Add acnchors to Kibana docs-settings (#78115) * Add acnchors to Kibana docs-settings * Address PR comments --- docs/settings/monitoring-settings.asciidoc | 12 +- docs/settings/reporting-settings.asciidoc | 18 +-- docs/settings/security-settings.asciidoc | 16 +-- docs/settings/telemetry-settings.asciidoc | 8 +- docs/setup/secure-settings.asciidoc | 2 +- docs/setup/settings.asciidoc | 154 +++++++++++---------- 6 files changed, 106 insertions(+), 104 deletions(-) diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 6c8632efa9cc0..917821ad09e2f 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -33,7 +33,7 @@ For more information, see |=== | `monitoring.enabled` | Set to `true` (default) to enable the {monitor-features} in {kib}. Unlike the - `monitoring.ui.enabled` setting, when this setting is `false`, the + <> setting, when this setting is `false`, the monitoring back-end does not run and {kib} stats are not sent to the monitoring cluster. @@ -44,7 +44,7 @@ a|`monitoring.cluster_alerts.` | `monitoring.ui.elasticsearch.hosts` | Specifies the location of the {es} cluster where your monitoring data is stored. - By default, this is the same as `elasticsearch.hosts`. This setting enables + By default, this is the same as <>. This setting enables you to use a single {kib} instance to search and visualize data in your production cluster as well as monitor data sent to a dedicated monitoring cluster. @@ -58,7 +58,7 @@ a|`monitoring.cluster_alerts.` cluster uses the authenticated user's credentials, which must be the same on both the {es} monitoring cluster and the {es} production cluster. + + - If not set, {kib} uses the value of the `elasticsearch.username` setting. + If not set, {kib} uses the value of the <> setting. | `monitoring.ui.elasticsearch.password` | Specifies the password used by {kib} monitoring to establish a persistent connection @@ -69,11 +69,11 @@ a|`monitoring.cluster_alerts.` cluster uses the authenticated user's credentials, which must be the same on both the {es} monitoring cluster and the {es} production cluster. + + - If not set, {kib} uses the value of the `elasticsearch.password` setting. + If not set, {kib} uses the value of the <> setting. | `monitoring.ui.elasticsearch.pingTimeout` | Specifies the time in milliseconds to wait for {es} to respond to internal - health checks. By default, it matches the `elasticsearch.pingTimeout` setting, + health checks. By default, it matches the <> setting, which has a default value of `30000`. |=== @@ -112,7 +112,7 @@ about configuring {kib}, see | Specifies the number of log entries to display in *{stack-monitor-app}*. Defaults to `10`. The maximum value is `50`. -| `monitoring.ui.enabled` +|[[monitoring-ui-enabled]] `monitoring.ui.enabled` | Set to `false` to hide *{stack-monitor-app}*. The monitoring back-end continues to run as an agent for sending {kib} stats to the monitoring cluster. Defaults to `true`. diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 27ef089f5847d..adfc3964d4204 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -20,7 +20,7 @@ You can configure `xpack.reporting` settings in your `kibana.yml` to: | [[xpack-enable-reporting]]`xpack.reporting.enabled` {ess-icon} | Set to `false` to disable the {report-features}. -| `xpack.reporting.encryptionKey` {ess-icon} +|[[xpack-reporting-encryptionKey]] `xpack.reporting.encryptionKey` {ess-icon} | Set to an alphanumeric, at least 32 characters long text string. By default, {kib} will generate a random key when it starts, which will cause pending reports to fail after restart. Configure this setting to preserve the same key across multiple restarts and multiple instances of {kib}. @@ -53,20 +53,20 @@ proxy host requires that the {kib} server has network access to the proxy. [cols="2*<"] |=== | `xpack.reporting.kibanaServer.port` - | The port for accessing {kib}, if different from the `server.port` value. + | The port for accessing {kib}, if different from the <> value. | `xpack.reporting.kibanaServer.protocol` | The protocol for accessing {kib}, typically `http` or `https`. -| `xpack.reporting.kibanaServer.hostname` - | The hostname for accessing {kib}, if different from the `server.host` value. +|[[xpack-kibanaServer-hostname]] `xpack.reporting.kibanaServer.hostname` + | The hostname for accessing {kib}, if different from the <> value. |=== [NOTE] ============ Reporting authenticates requests on the Kibana page only when the hostname matches the -`xpack.reporting.kibanaServer.hostname` setting. Therefore Reporting would fail if the +<> setting. Therefore Reporting would fail if the set value redirects to another server. For that reason, `"0"` is an invalid setting because, in the Reporting browser, it becomes an automatic redirect to `"0.0.0.0"`. ============ @@ -97,8 +97,8 @@ reports, you might need to change the following settings. [NOTE] ============ Running multiple instances of {kib} in a cluster for load balancing of -reporting requires identical values for `xpack.reporting.encryptionKey` and, if -security is enabled, `xpack.security.encryptionKey`. +reporting requires identical values for <> and, if +security is enabled, <>. ============ [cols="2*<"] @@ -177,7 +177,7 @@ available, but there will likely be errors in the visualizations in the report. [[reporting-chromium-settings]] ==== Chromium settings -When `xpack.reporting.capture.browser.type` is set to `chromium` (default) you can also specify the following settings. +When <> is set to `chromium` (default) you can also specify the following settings. [cols="2*<"] |=== @@ -246,7 +246,7 @@ a| `xpack.reporting.capture.browser` | Reporting uses a weekly index in {es} to store the reporting job and the report content. The index is automatically created if it does not already exist. Configure this to a unique value, beginning with `.reporting-`, for every - {kib} instance that has a unique `kibana.index` setting. Defaults to `.reporting`. + {kib} instance that has a unique <> setting. Defaults to `.reporting`. | `xpack.reporting.roles.allow` | Specifies the roles in addition to superusers that can use reporting. diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index b6eecc6ea9f04..00e5f973f7d87 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -190,26 +190,26 @@ You can configure the following settings in the `kibana.yml` file. | `xpack.security.cookieName` | Sets the name of the cookie used for the session. The default value is `"sid"`. -| `xpack.security.encryptionKey` +|[[xpack-security-encryptionKey]] `xpack.security.encryptionKey` | An arbitrary string of 32 characters or more that is used to encrypt session information. Do **not** expose this key to users of {kib}. By default, a value is automatically generated in memory. If you use that default behavior, all sessions are invalidated when {kib} restarts. In addition, high-availability deployments of {kib} will behave unexpectedly if this setting isn't the same for all instances of {kib}. -| `xpack.security.secureCookies` +|[[xpack-security-secureCookies]] `xpack.security.secureCookies` | Sets the `secure` flag of the session cookie. The default value is `false`. It - is automatically set to `true` if `server.ssl.enabled` is set to `true`. Set + is automatically set to `true` if <> is set to `true`. Set this to `true` if SSL is configured outside of {kib} (for example, you are routing requests through a load balancer or proxy). | `xpack.security.sameSiteCookies` {ess-icon} | Sets the `SameSite` attribute of the session cookie. This allows you to declare whether your cookie should be restricted to a first-party or same-site context. Valid values are `Strict`, `Lax`, `None`. - This is *not set* by default, which modern browsers will treat as `Lax`. If you use Kibana embedded in an iframe in modern browsers, you might need to set it to `None`. Setting this value to `None` requires cookies to be sent over a secure connection by setting `xpack.security.secureCookies: true`. + This is *not set* by default, which modern browsers will treat as `Lax`. If you use Kibana embedded in an iframe in modern browsers, you might need to set it to `None`. Setting this value to `None` requires cookies to be sent over a secure connection by setting <>: `true`. -| `xpack.security.session.idleTimeout` {ess-icon} - | Ensures that user sessions will expire after a period of inactivity. This and `xpack.security.session.lifespan` are both +|[[xpack-session-idleTimeout]] `xpack.security.session.idleTimeout` {ess-icon} + | Ensures that user sessions will expire after a period of inactivity. This and <> are both highly recommended. By default, this setting is not set. 2+a| @@ -218,9 +218,9 @@ highly recommended. By default, this setting is not set. The format is a string of `[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w'). ============ -| `xpack.security.session.lifespan` {ess-icon} +|[[xpack-session-lifespan]] `xpack.security.session.lifespan` {ess-icon} | Ensures that user sessions will expire after the defined time period. This behavior also known as an "absolute timeout". If -this is _not_ set, user sessions could stay active indefinitely. This and `xpack.security.session.idleTimeout` are both highly +this is _not_ set, user sessions could stay active indefinitely. This and <> are both highly recommended. By default, this setting is not set. 2+a| diff --git a/docs/settings/telemetry-settings.asciidoc b/docs/settings/telemetry-settings.asciidoc index 89c018a86eca6..0329e2f010e80 100644 --- a/docs/settings/telemetry-settings.asciidoc +++ b/docs/settings/telemetry-settings.asciidoc @@ -19,7 +19,7 @@ See our https://www.elastic.co/legal/privacy-statement[Privacy Statement] to lea [cols="2*<"] |=== -| `telemetry.enabled` +|[[telemetry-enabled]] `telemetry.enabled` | Set to `true` to send cluster statistics to Elastic. Reporting your cluster statistics helps us improve your user experience. Your data is never shared with anyone. Set to `false` to disable statistics reporting from any @@ -31,16 +31,16 @@ See our https://www.elastic.co/legal/privacy-statement[Privacy Statement] to lea it is behind a firewall and falls back to `'browser'` to send it from users' browsers when they are navigating through {kib}. Defaults to `'server'`. -| `telemetry.optIn` +|[[telemetry-optIn]] `telemetry.optIn` | Set to `true` to automatically opt into reporting cluster statistics. You can also opt out through *Advanced Settings* in {kib}. Defaults to `true`. | `telemetry.allowChangingOptInStatus` - | Set to `true` to allow overwriting the `telemetry.optIn` setting via the {kib} UI. Defaults to `true`. + + | Set to `true` to allow overwriting the <> setting via the {kib} UI. Defaults to `true`. + |=== [NOTE] ============ -When `false`, `telemetry.optIn` must be `true`. To disable telemetry and not allow users to change that parameter, use `telemetry.enabled`. +When `false`, <> must be `true`. To disable telemetry and not allow users to change that parameter, use <>. ============ diff --git a/docs/setup/secure-settings.asciidoc b/docs/setup/secure-settings.asciidoc index 10380eb5d8fa4..840a18ac03bab 100644 --- a/docs/setup/secure-settings.asciidoc +++ b/docs/setup/secure-settings.asciidoc @@ -19,7 +19,7 @@ bin/kibana-keystore create ---------------------------------------------------------------- The file `kibana.keystore` will be created in the directory defined by the -`path.data` configuration setting. +<> configuration setting. [float] [[list-settings]] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index f03022e9e9f00..7f48f21db7197 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -20,11 +20,11 @@ which may cause a delay before pages start being served. Set to `false` to disable Console. *Default: `true`* | `cpu.cgroup.path.override:` - | *deprecated* This setting has been renamed to `ops.cGroupOverrides.cpuPath` + | *deprecated* This setting has been renamed to <> and the old name will no longer be supported as of 8.0. | `cpuacct.cgroup.path.override:` - | *deprecated* This setting has been renamed to `ops.cGroupOverrides.cpuAcctPath` + | *deprecated* This setting has been renamed to <> and the old name will no longer be supported as of 8.0. | `csp.rules:` @@ -33,7 +33,7 @@ that disables certain unnecessary and potentially insecure capabilities in the browser. It is strongly recommended that you keep the default CSP rules that ship with {kib}. -| `csp.strict:` +|[[csp-strict]] `csp.strict:` | Blocks {kib} access to any browser that does not enforce even rudimentary CSP rules. In practice, this disables support for older, less safe browsers like Internet Explorer. @@ -43,15 +43,15 @@ For more information, refer to <>. | `csp.warnLegacyBrowsers:` | Shows a warning message after loading {kib} to any browser that does not enforce even rudimentary CSP rules, though {kib} is still accessible. This -configuration is effectively ignored when `csp.strict` is enabled. +configuration is effectively ignored when <> is enabled. *Default: `true`* | `elasticsearch.customHeaders:` | Header names and values to send to {es}. Any custom headers cannot be overwritten by client-side headers, regardless of the -`elasticsearch.requestHeadersWhitelist` configuration. *Default: `{}`* +<> configuration. *Default: `{}`* -| `elasticsearch.hosts:` +|[[elasticsearch-hosts]] `elasticsearch.hosts:` | The URLs of the {es} instances to use for all your queries. All nodes listed here must be on the same cluster. *Default: `[ "http://localhost:9200" ]`* + @@ -59,28 +59,28 @@ To enable SSL/TLS for outbound connections to {es}, use the `https` protocol in this setting. | `elasticsearch.logQueries:` - | Log queries sent to {es}. Requires `logging.verbose` set to `true`. + | Log queries sent to {es}. Requires <> set to `true`. This is useful for seeing the query DSL generated by applications that currently do not have an inspector, for example Timelion and Monitoring. *Default: `false`* -| `elasticsearch.pingTimeout:` +|[[elasticsearch-pingTimeout]] `elasticsearch.pingTimeout:` | Time in milliseconds to wait for {es} to respond to pings. -*Default: the value of the `elasticsearch.requestTimeout` setting* +*Default: the value of the <> setting* | `elasticsearch.preserveHost:` | When the value is `true`, {kib} uses the hostname specified in the -`server.host` setting. When the value is `false`, {kib} uses +<> setting. When the value is `false`, {kib} uses the hostname of the host that connects to this {kib} instance. *Default: `true`* -| `elasticsearch.requestHeadersWhitelist:` +|[[elasticsearch-requestHeadersWhitelist]] `elasticsearch.requestHeadersWhitelist:` | List of {kib} client-side headers to send to {es}. To send *no* client-side headers, set this value to [] (an empty list). Removing the `authorization` header from being whitelisted means that you cannot use <> in {kib}. *Default: `[ 'authorization' ]`* -| `elasticsearch.requestTimeout:` +|[[elasticsearch-requestTimeout]] `elasticsearch.requestTimeout:` | Time in milliseconds to wait for responses from the back end or {es}. This value must be a positive integer. *Default: `30000`* @@ -99,7 +99,7 @@ nodes. *Default: `false`* | Update the list of {es} nodes immediately following a connection fault. *Default: `false`* -| `elasticsearch.ssl.alwaysPresentCertificate:` +|[[elasticsearch-ssl-alwaysPresentCertificate]] `elasticsearch.ssl.alwaysPresentCertificate:` | Controls {kib} behavior in regard to presenting a client certificate when requested by {es}. This setting applies to all outbound SSL/TLS connections to {es}, including requests that are proxied for end users. *Default: `false`* @@ -109,7 +109,7 @@ to {es}, including requests that are proxied for end users. *Default: `false`* [WARNING] ============ When {es} uses certificates to authenticate end users with a PKI realm -and `elasticsearch.ssl.alwaysPresentCertificate` is `true`, +and <> is `true`, proxied requests may be executed as the identity that is tied to the {kib} server. ============ @@ -117,7 +117,7 @@ server. [cols="2*<"] |=== -| `elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:` +|[[elasticsearch-ssl-cert-key]] `elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:` | Paths to a PEM-encoded X.509 client certificate and its corresponding private key. These are used by {kib} to authenticate itself when making outbound SSL/TLS connections to {es}. For this setting to take effect, the @@ -129,27 +129,29 @@ be set to `"required"` or `"optional"` to request a client certificate from [NOTE] ============ -These settings cannot be used in conjunction with `elasticsearch.ssl.keystore.path`. +These settings cannot be used in conjunction with +<>. ============ [cols="2*<"] |=== -| `elasticsearch.ssl.certificateAuthorities:` +|[[elasticsearch-ssl-certificateAuthorities]] `elasticsearch.ssl.certificateAuthorities:` | Paths to one or more PEM-encoded X.509 certificate authority (CA) certificates, which make up a trusted certificate chain for {es}. This chain is used by {kib} to establish trust when making outbound SSL/TLS connections to {es}. + In addition to this setting, trusted certificates may be specified via -`elasticsearch.ssl.keystore.path` and/or `elasticsearch.ssl.truststore.path`. +<> and/or +<>. | `elasticsearch.ssl.keyPassphrase:` | The password that decrypts the private key that is specified -via `elasticsearch.ssl.key`. This value is optional, as the key may not be +via <>. This value is optional, as the key may not be encrypted. -| `elasticsearch.ssl.keystore.path:` +|[[elasticsearch-ssl-keystore-path]] `elasticsearch.ssl.keystore.path:` | Path to a PKCS#12 keystore that contains an X.509 client certificate and it's corresponding private key. These are used by {kib} to authenticate itself when making outbound SSL/TLS connections to {es}. For this setting, you must also set @@ -160,15 +162,15 @@ If the keystore contains any additional certificates, they are used as a trusted certificate chain for {es}. This chain is used by {kib} to establish trust when making outbound SSL/TLS connections to {es}. In addition to this setting, trusted certificates may be specified via -`elasticsearch.ssl.certificateAuthorities` and/or -`elasticsearch.ssl.truststore.path`. +<> and/or +<>. |=== [NOTE] ============ This setting cannot be used in conjunction with -`elasticsearch.ssl.certificate` or `elasticsearch.ssl.key`. +<> or <>. ============ [cols="2*<"] @@ -176,24 +178,24 @@ This setting cannot be used in conjunction with | `elasticsearch.ssl.keystore.password:` | The password that decrypts the keystore specified via -`elasticsearch.ssl.keystore.path`. If the keystore has no password, leave this +<>. If the keystore has no password, leave this as blank. If the keystore has an empty password, set this to `""`. -| `elasticsearch.ssl.truststore.path:`:: +|[[elasticsearch-ssl-truststore-path]] `elasticsearch.ssl.truststore.path:` | Path to a PKCS#12 trust store that contains one or more X.509 certificate authority (CA) certificates, which make up a trusted certificate chain for {es}. This chain is used by {kib} to establish trust when making outbound SSL/TLS connections to {es}. + In addition to this setting, trusted certificates may be specified via -`elasticsearch.ssl.certificateAuthorities` and/or -`elasticsearch.ssl.keystore.path`. +<> and/or +<>. |`elasticsearch.ssl.truststore.password:` | The password that decrypts the trust store specified via -`elasticsearch.ssl.truststore.path`. If the trust store has no password, -leave this as blank. If the trust store has an empty password, set this to `""`. +<>. If the trust store +has no password, leave this as blank. If the trust store has an empty password, set this to `""`. | `elasticsearch.ssl.verificationMode:` | Controls the verification of the server certificate that {kib} receives when @@ -206,7 +208,7 @@ verification entirely. *Default: `"full"`* | Time in milliseconds to wait for {es} at {kib} startup before retrying. *Default: `5000`* -| `elasticsearch.username:` and `elasticsearch.password:` +|[[elasticsearch-user-passwd]] `elasticsearch.username:` and `elasticsearch.password:` | If your {es} is protected with basic authentication, these settings provide the username and password that the {kib} server uses to perform maintenance on the {kib} index at startup. {kib} users still need to authenticate with @@ -220,7 +222,7 @@ on the {kib} index at startup. {kib} users still need to authenticate with Please use the `defaultRoute` advanced setting instead. The default application to load. *Default: `"home"`* -| `kibana.index:` +|[[kibana-index]] `kibana.index:` | {kib} uses an index in {es} to store saved searches, visualizations, and dashboards. {kib} creates a new index if the index doesn’t already exist. If you configure a custom index, the name must be lowercase, and conform to the @@ -236,7 +238,7 @@ This value must be a whole number greater than zero. *Default: `"1000"`* suggestions. This value must be a whole number greater than zero. *Default: `"100000"`* -| `logging.dest:` +|[[logging-dest]] `logging.dest:` | Enables you to specify a file where {kib} stores log output. *Default: `stdout`* @@ -244,7 +246,7 @@ suggestions. This value must be a whole number greater than zero. | Logs output as JSON. When set to `true`, the logs are formatted as JSON strings that include timestamp, log level, context, message text, and any other metadata that may be associated with the log message. -When `logging.dest.stdout` is set, and there is no interactive terminal ("TTY"), +When <> is set, and there is no interactive terminal ("TTY"), this setting defaults to `true`. *Default: `false`* | `logging.quiet:` @@ -271,7 +273,7 @@ The following example shows a valid logging rotate configuration: | `logging.rotate.enabled:` | experimental[] Set the value of this setting to `true` to -enable log rotation. If you do not have a `logging.dest` set that is different from `stdout` +enable log rotation. If you do not have a <> set that is different from `stdout` that feature would not take any effect. *Default: `false`* | `logging.rotate.everyBytes:` @@ -286,9 +288,9 @@ option has to be in the range of 2 to 1024 files. *Default: `7`* | `logging.rotate.pollingInterval:` | experimental[] The number of milliseconds for the polling strategy in case -the `logging.rotate.usePolling` is enabled. `logging.rotate.usePolling` must be in the 5000 to 3600000 millisecond range. *Default: `10000`* +the <> is enabled. `logging.rotate.usePolling` must be in the 5000 to 3600000 millisecond range. *Default: `10000`* -| `logging.rotate.usePolling:` +|[[logging-rotate-usePolling]] `logging.rotate.usePolling:` | experimental[] By default we try to understand the best way to monitoring the log file and warning about it. Please be aware there are some systems where watch api is not accurate. In those cases, in order to get the feature working, the `polling` method could be used enabling that option. *Default: `false`* @@ -308,8 +310,8 @@ requests. *Default: `false`* | `map.includeElasticMapsService:` {ess-icon} | Set to `false` to disable connections to Elastic Maps Service. -When `includeElasticMapsService` is turned off, only the vector layers configured by `map.regionmap` -and the tile layer configured by `map.tilemap.url` are available in <>. *Default: `true`* +When `includeElasticMapsService` is turned off, only the vector layers configured by <> +and the tile layer configured by <> are available in <>. *Default: `true`* | `map.proxyElasticMapsServiceInMaps:` | Set to `true` to proxy all <> Elastic Maps Service @@ -427,7 +429,7 @@ override this parameter to use their own Tile Map Service. For example: system for the {kib} UI notification center. Set to `false` to disable the newsfeed system. *Default: `true`* -| `path.data:` +|[[path-data]] `path.data:` | The path where {kib} stores persistent data not saved in {es}. *Default: `data`* @@ -438,17 +440,17 @@ not saved in {es}. *Default: `data`* | Set the interval in milliseconds to sample system and process performance metrics. The minimum value is 100. *Default: `5000`* -| `ops.cGroupOverrides.cpuPath:` +|[[ops-cGroupOverrides-cpuPath]] `ops.cGroupOverrides.cpuPath:` | Override for cgroup cpu path when mounted in a manner that is inconsistent with `/proc/self/cgroup`. -| `ops.cGroupOverrides.cpuAcctPath:` +|[[ops-cGroupOverrides-cpuAcctPath]] `ops.cGroupOverrides.cpuAcctPath:` | Override for cgroup cpuacct path when mounted in a manner that is inconsistent with `/proc/self/cgroup`. -| `server.basePath:` +|[[server-basePath]] `server.basePath:` | Enables you to specify a path to mount {kib} at if you are -running behind a proxy. Use the `server.rewriteBasePath` setting to tell {kib} +running behind a proxy. Use the <> setting to tell {kib} if it should remove the basePath from requests it receives, and to prevent a deprecation warning at startup. This setting cannot end in a slash (`/`). @@ -458,19 +460,19 @@ deprecation warning at startup. This setting cannot end in a slash (`/`). | `server.compression.referrerWhitelist:` | Specifies an array of trusted hostnames, such as the {kib} host, or a reverse proxy sitting in front of it. This determines whether HTTP compression may be used for responses, based on the request `Referer` header. -This setting may not be used when `server.compression.enabled` is set to `false`. *Default: `none`* +This setting may not be used when <> is set to `false`. *Default: `none`* | `server.customResponseHeaders:` {ess-icon} | Header names and values to send on all responses to the client from the {kib} server. *Default: `{}`* -| `server.host:` +|[[server-host]] `server.host:` | This setting specifies the host of the back end server. To allow remote users to connect, set the value to the IP address or DNS name of the {kib} server. *Default: `"localhost"`* | `server.keepaliveTimeout:` | The number of milliseconds to wait for additional data before restarting -the `server.socketTimeout` counter. *Default: `"120000"`* +the <> counter. *Default: `"120000"`* | `server.maxPayloadBytes:` | The maximum payload size in bytes @@ -480,28 +482,28 @@ for incoming server requests. *Default: `1048576`* | A human-readable display name that identifies this {kib} instance. *Default: `"your-hostname"`* -| `server.port:` +|[[server-port]] `server.port:` | {kib} is served by a back end server. This setting specifies the port to use. *Default: `5601`* -| `server.requestId.allowFromAnyIp:` +|[[server-requestId-allowFromAnyIp]] `server.requestId.allowFromAnyIp:` | Sets whether or not the X-Opaque-Id header should be trusted from any IP address for identifying requests in logs and forwarded to Elasticsearch. | `server.requestId.ipAllowlist:` - | A list of IPv4 and IPv6 address which the `X-Opaque-Id` header should be trusted from. Normally this would be set to the IP addresses of the load balancers or reverse-proxy that end users use to access Kibana. If any are set, `server.requestId.allowFromAnyIp` must also be set to `false.` + | A list of IPv4 and IPv6 address which the `X-Opaque-Id` header should be trusted from. Normally this would be set to the IP addresses of the load balancers or reverse-proxy that end users use to access Kibana. If any are set, <> must also be set to `false.` -| `server.rewriteBasePath:` +|[[server-rewriteBasePath]] `server.rewriteBasePath:` | Specifies whether {kib} should -rewrite requests that are prefixed with `server.basePath` or require that they +rewrite requests that are prefixed with <> or require that they are rewritten by your reverse proxy. In {kib} 6.3 and earlier, the default is `false`. In {kib} 7.x, the setting is deprecated. In {kib} 8.0 and later, the default is `true`. *Default: `deprecated`* -| `server.socketTimeout:` +|[[server-socketTimeout]] `server.socketTimeout:` | The number of milliseconds to wait before closing an inactive socket. *Default: `"120000"`* -| `server.ssl.certificate:` and `server.ssl.key:` +|[[server-ssl-cert-key]] `server.ssl.certificate:` and `server.ssl.key:` | Paths to a PEM-encoded X.509 server certificate and its corresponding private key. These are used by {kib} to establish trust when receiving inbound SSL/TLS connections from users. @@ -509,18 +511,18 @@ are used by {kib} to establish trust when receiving inbound SSL/TLS connections [NOTE] ============ -These settings cannot be used in conjunction with `server.ssl.keystore.path`. +These settings cannot be used in conjunction with <>. ============ [cols="2*<"] |=== -| `server.ssl.certificateAuthorities:` +|[[server-ssl-certificateAuthorities]] `server.ssl.certificateAuthorities:` | Paths to one or more PEM-encoded X.509 certificate authority (CA) certificates which make up a trusted certificate chain for {kib}. This chain is used by {kib} to establish trust when receiving inbound SSL/TLS connections from end users. If PKI authentication is enabled, this chain is also used by {kib} to verify client certificates from end users. + -In addition to this setting, trusted certificates may be specified via `server.ssl.keystore.path` and/or `server.ssl.truststore.path`. +In addition to this setting, trusted certificates may be specified via <> and/or <>. | `server.ssl.cipherSuites:` | Details on the format, and the valid options, are available via the @@ -533,53 +535,53 @@ connections. Valid values are `"required"`, `"optional"`, and `"none"`. Using `" client presents a certificate, using `"optional"` will allow a client to present a certificate if it has one, and using `"none"` will prevent a client from presenting a certificate. *Default: `"none"`* -| `server.ssl.enabled:` +|[[server-ssl-enabled]] `server.ssl.enabled:` | Enables SSL/TLS for inbound connections to {kib}. When set to `true`, a certificate and its -corresponding private key must be provided. These can be specified via `server.ssl.keystore.path` or the combination of -`server.ssl.certificate` and `server.ssl.key`. *Default: `false`* +corresponding private key must be provided. These can be specified via <> or the combination of +<> and <>. *Default: `false`* | `server.ssl.keyPassphrase:` - | The password that decrypts the private key that is specified via `server.ssl.key`. This value + | The password that decrypts the private key that is specified via <>. This value is optional, as the key may not be encrypted. -| `server.ssl.keystore.path:` +|[[server-ssl-keystore-path]] `server.ssl.keystore.path:` | Path to a PKCS#12 keystore that contains an X.509 server certificate and its corresponding private key. If the keystore contains any additional certificates, those will be used as a trusted certificate chain for {kib}. All of these are used by {kib} to establish trust when receiving inbound SSL/TLS connections from end users. The certificate chain is also used by {kib} to verify client certificates from end users when PKI authentication is enabled. + -In addition to this setting, trusted certificates may be specified via `server.ssl.certificateAuthorities` and/or -`server.ssl.truststore.path`. +In addition to this setting, trusted certificates may be specified via <> and/or +<>. |=== [NOTE] ============ -This setting cannot be used in conjunction with `server.ssl.certificate` or `server.ssl.key` +This setting cannot be used in conjunction with <> or <> ============ [cols="2*<"] |=== | `server.ssl.keystore.password:` - | The password that will be used to decrypt the keystore specified via `server.ssl.keystore.path`. If the + | The password that will be used to decrypt the keystore specified via <>. If the keystore has no password, leave this unset. If the keystore has an empty password, set this to `""`. -| `server.ssl.truststore.path:` +|[[server-ssl-truststore-path]] `server.ssl.truststore.path:` | Path to a PKCS#12 trust store that contains one or more X.509 certificate authority (CA) certificates which make up a trusted certificate chain for {kib}. This chain is used by {kib} to establish trust when receiving inbound SSL/TLS connections from end users. If PKI authentication is enabled, this chain is also used by {kib} to verify client certificates from end users. + -In addition to this setting, trusted certificates may be specified via `server.ssl.certificateAuthorities` and/or -`server.ssl.keystore.path`. +In addition to this setting, trusted certificates may be specified via <> and/or +<>. | `server.ssl.truststore.password:` - | The password that will be used to decrypt the trust store specified via `server.ssl.truststore.path`. If + | The password that will be used to decrypt the trust store specified via <>. If the trust store has no password, leave this unset. If the trust store has an empty password, set this to `""`. | `server.ssl.redirectHttpFromPort:` | {kib} binds to this port and redirects -all http requests to https over the port configured as `server.port`. +all http requests to https over the port configured as <>. | `server.ssl.supportedProtocols:` | An array of supported protocols with versions. @@ -588,7 +590,7 @@ Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2`. *Default: TLSv1.1, TLSv1.2* | [[settings-xsrf-whitelist]] `server.xsrf.whitelist:` | It is not recommended to disable protections for arbitrary API endpoints. Instead, supply the `kbn-xsrf` header. -The `server.xsrf.whitelist` setting requires the following format: +The <> setting requires the following format: |=== @@ -608,18 +610,18 @@ The `server.xsrf.whitelist` setting requires the following format: setting this to `true` enables unauthenticated users to access the {kib} server status API and status page. *Default: `false`* -| `telemetry.allowChangingOptInStatus` +|[[telemetry-allowChangingOptInStatus]] `telemetry.allowChangingOptInStatus` | When `true`, users are able to change the telemetry setting at a later time in <>. When `false`, -{kib} looks at the value of `telemetry.optIn` to determine whether to send -telemetry data or not. `telemetry.allowChangingOptInStatus` and `telemetry.optIn` +{kib} looks at the value of <> to determine whether to send +telemetry data or not. <> and <> cannot be `false` at the same time. *Default: `true`*. -| `telemetry.optIn` +|[[settings-telemetry-optIn]] `telemetry.optIn` | When `true`, telemetry data is sent to Elastic. When `false`, collection of telemetry data is disabled. To enable telemetry and prevent users from disabling it, -set `telemetry.allowChangingOptInStatus` to `false` and `telemetry.optIn` to `true`. +set <> to `false` and <> to `true`. *Default: `true`* | `telemetry.enabled` From 0fbef00de26194d387e418ff23bd79174eebbcea Mon Sep 17 00:00:00 2001 From: Shamin Meerankutty <8272719+shamin@users.noreply.github.com> Date: Wed, 23 Sep 2020 20:52:18 +0530 Subject: [PATCH 48/92] Fix canvas table not inheriting font size and text align (#75149) * Fix canvas table not inheriting font size and align * Removed the font-size and text-align properties Co-authored-by: Elastic Machine --- .../plugins/canvas/public/components/datatable/datatable.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugins/canvas/public/components/datatable/datatable.scss b/x-pack/plugins/canvas/public/components/datatable/datatable.scss index bd11bff18e091..8e36de3b84423 100644 --- a/x-pack/plugins/canvas/public/components/datatable/datatable.scss +++ b/x-pack/plugins/canvas/public/components/datatable/datatable.scss @@ -4,7 +4,6 @@ display: flex; flex-direction: column; justify-content: space-between; - font-size: $euiFontSizeS; .canvasDataTable__tableWrapper { @include euiScrollBar; @@ -33,7 +32,6 @@ .canvasDataTable__th, .canvasDataTable__td { - text-align: left; padding: $euiSizeS $euiSizeXS; border-bottom: $euiBorderThin; } From d3fc2ff166729c4f4a6311ccee51bde2d6e4790f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Wed, 23 Sep 2020 17:48:31 +0200 Subject: [PATCH 49/92] [Security Solution] Cleanup Uncommon Processes graphql (#78271) --- .../hosts/uncommon_processes/index.ts | 45 +-- .../security_solution/index.ts | 8 +- .../public/graphql/introspection.json | 234 ---------------- .../security_solution/public/graphql/types.ts | 144 ---------- .../uncommon_process_table/index.test.tsx | 108 +++---- .../uncommon_process_table/index.tsx | 21 +- .../components/uncommon_process_table/mock.ts | 187 ++++++------ .../uncommon_processes/index.gql_query.ts | 59 ---- .../containers/uncommon_processes/index.tsx | 23 +- .../security_solution/server/graphql/index.ts | 2 - .../security_solution/server/graphql/types.ts | 151 ---------- .../graphql/uncommon_processes/index.ts | 8 - .../graphql/uncommon_processes/resolvers.ts | 35 --- .../graphql/uncommon_processes/schema.gql.ts | 39 --- .../security_solution/server/init_server.ts | 2 - .../server/lib/compose/kibana.ts | 2 - .../security_solution/server/lib/types.ts | 2 - .../elasticsearch_adapter.test.ts | 265 ------------------ .../elasticsearch_adapter.ts | 118 -------- .../server/lib/uncommon_processes/index.ts | 21 -- .../lib/uncommon_processes/query.dsl.ts | 222 --------------- .../server/lib/uncommon_processes/types.ts | 54 ---- .../hosts/uncommon_processes/helpers.test.ts | 12 +- .../hosts/uncommon_processes/helpers.ts | 14 +- .../hosts/uncommon_processes/index.test.ts | 4 +- .../factory/hosts/uncommon_processes/index.ts | 10 +- .../apis/security_solution/index.js | 2 +- .../security_solution/uncommon_processes.ts | 2 + 28 files changed, 192 insertions(+), 1602 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.gql_query.ts delete mode 100644 x-pack/plugins/security_solution/server/graphql/uncommon_processes/index.ts delete mode 100644 x-pack/plugins/security_solution/server/graphql/uncommon_processes/resolvers.ts delete mode 100644 x-pack/plugins/security_solution/server/graphql/uncommon_processes/schema.gql.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/uncommon_processes/index.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/uncommon_processes/query.dsl.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/uncommon_processes/types.ts diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/uncommon_processes/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/uncommon_processes/index.ts index 28c0ccb7f6f4f..19fc9333de7a4 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/uncommon_processes/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/uncommon_processes/index.ts @@ -7,6 +7,7 @@ import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/comm import { HostEcs } from '../../../../ecs/host'; import { UserEcs } from '../../../../ecs/user'; +import { ProcessEcs } from '../../../../ecs/process'; import { RequestOptionsPaginated, SortField, @@ -20,56 +21,32 @@ import { Hits, } from '../../..'; -export interface HostUncommonProcessesRequestOptions extends RequestOptionsPaginated { +export interface HostsUncommonProcessesRequestOptions extends RequestOptionsPaginated { sort: SortField; defaultIndex: string[]; } -export interface HostUncommonProcessesStrategyResponse extends IEsSearchResponse { - edges: UncommonProcessesEdges[]; +export interface HostsUncommonProcessesStrategyResponse extends IEsSearchResponse { + edges: HostsUncommonProcessesEdges[]; totalCount: number; pageInfo: PageInfoPaginated; inspect?: Maybe; } -export interface UncommonProcessesEdges { - node: UncommonProcessItem; +export interface HostsUncommonProcessesEdges { + node: HostsUncommonProcessItem; cursor: CursorType; } -export interface UncommonProcessItem { +export interface HostsUncommonProcessItem { _id: string; instances: number; - process: ProcessEcsFields; + process: ProcessEcs; hosts: HostEcs[]; user?: Maybe; } -export interface ProcessEcsFields { - hash?: Maybe; - pid?: Maybe; - name?: Maybe; - ppid?: Maybe; - args?: Maybe; - entity_id?: Maybe; - executable?: Maybe; - title?: Maybe; - thread?: Maybe; - working_directory?: Maybe; -} - -export interface ProcessHashData { - md5?: Maybe; - sha1?: Maybe; - sha256?: Maybe; -} - -export interface Thread { - id?: Maybe; - start?: Maybe; -} - -export interface UncommonProcessHit extends Hit { +export interface HostsUncommonProcessHit extends Hit { total: TotalHit; host: Array<{ id: string[] | undefined; @@ -77,10 +54,10 @@ export interface UncommonProcessHit extends Hit { }>; _source: { '@timestamp': string; - process: ProcessEcsFields; + process: ProcessEcs; }; cursor: string; sort: StringOrNumber[]; } -export type ProcessHits = Hits; +export type ProcessHits = Hits; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index cfcf613b662bc..af9faef89af46 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -18,8 +18,8 @@ import { HostsQueries, HostsRequestOptions, HostsStrategyResponse, - HostUncommonProcessesStrategyResponse, - HostUncommonProcessesRequestOptions, + HostsUncommonProcessesStrategyResponse, + HostsUncommonProcessesRequestOptions, HostsKpiQueries, HostsKpiAuthenticationsStrategyResponse, HostsKpiAuthenticationsRequestOptions, @@ -113,7 +113,7 @@ export type StrategyResponseType = T extends HostsQ : T extends HostsQueries.firstLastSeen ? HostFirstLastSeenStrategyResponse : T extends HostsQueries.uncommonProcesses - ? HostUncommonProcessesStrategyResponse + ? HostsUncommonProcessesStrategyResponse : T extends HostsKpiQueries.kpiAuthentications ? HostsKpiAuthenticationsStrategyResponse : T extends HostsKpiQueries.kpiHosts @@ -161,7 +161,7 @@ export type StrategyRequestType = T extends HostsQu : T extends HostsQueries.firstLastSeen ? HostFirstLastSeenRequestOptions : T extends HostsQueries.uncommonProcesses - ? HostUncommonProcessesRequestOptions + ? HostsUncommonProcessesRequestOptions : T extends HostsKpiQueries.kpiAuthentications ? HostsKpiAuthenticationsRequestOptions : T extends HostsKpiQueries.kpiHosts diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 0bbc1fcc80e92..9e6a4f21ec64f 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -2186,67 +2186,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "UncommonProcesses", - "description": "Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified", - "args": [ - { - "name": "timerange", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "pagination", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "PaginationInputPaginated", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "filterQuery", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "defaultIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "UncommonProcessesData", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "whoAmI", "description": "Just a simple example to get the app name", @@ -9347,179 +9286,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "UncommonProcessesData", - "description": "", - "fields": [ - { - "name": "edges", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "UncommonProcessesEdges", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "totalCount", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "PageInfoPaginated", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UncommonProcessesEdges", - "description": "", - "fields": [ - { - "name": "node", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "UncommonProcessItem", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "cursor", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "CursorType", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "UncommonProcessItem", - "description": "", - "fields": [ - { - "name": "_id", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "instances", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "process", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "ProcessEcsFields", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "hosts", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "HostEcsFields", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "user", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "UserEcsFields", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "SayMyName", diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 4d3837f434b05..1699ac4dd33eb 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -558,8 +558,6 @@ export interface Source { OverviewNetwork?: Maybe; OverviewHost?: Maybe; - /** Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified */ - UncommonProcesses: UncommonProcessesData; /** Just a simple example to get the app name */ whoAmI?: Maybe; } @@ -1916,34 +1914,6 @@ export interface OverviewHostData { inspect?: Maybe; } -export interface UncommonProcessesData { - edges: UncommonProcessesEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe; -} - -export interface UncommonProcessesEdges { - node: UncommonProcessItem; - - cursor: CursorType; -} - -export interface UncommonProcessItem { - _id: string; - - instances: number; - - process: ProcessEcsFields; - - hosts: HostEcsFields[]; - - user?: Maybe; -} - export interface SayMyName { /** The id of the source */ appName: string; @@ -2531,15 +2501,6 @@ export interface OverviewHostSourceArgs { defaultIndex: string[]; } -export interface UncommonProcessesSourceArgs { - timerange: TimerangeInput; - - pagination: PaginationInputPaginated; - - filterQuery?: Maybe; - - defaultIndex: string[]; -} export interface IndicesExistSourceStatusArgs { defaultIndex: string[]; } @@ -3241,111 +3202,6 @@ export namespace GetKpiHostsQuery { }; } -export namespace GetUncommonProcessesQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - pagination: PaginationInputPaginated; - filterQuery?: Maybe; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - UncommonProcesses: UncommonProcesses; - }; - - export type UncommonProcesses = { - __typename?: 'UncommonProcessesData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe; - }; - - export type Edges = { - __typename?: 'UncommonProcessesEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'UncommonProcessItem'; - - _id: string; - - instances: number; - - process: Process; - - user: Maybe; - - hosts: Hosts[]; - }; - - export type Process = { - __typename?: 'ProcessEcsFields'; - - args: Maybe; - - name: Maybe; - }; - - export type User = { - __typename?: 'UserEcsFields'; - - id: Maybe; - - name: Maybe; - }; - - export type Hosts = { - __typename?: 'HostEcsFields'; - - name: Maybe; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - export namespace GetIpOverviewQuery { export type Variables = { sourceId: string; diff --git a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx index 5ace3439a2de6..41f443f14cafe 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx @@ -30,18 +30,14 @@ describe('Uncommon Process Table Component', () => { const wrapper = shallow( @@ -54,18 +50,14 @@ describe('Uncommon Process Table Component', () => { const wrapper = mount( @@ -79,18 +71,14 @@ describe('Uncommon Process Table Component', () => { const wrapper = mount( @@ -105,18 +93,14 @@ describe('Uncommon Process Table Component', () => { const wrapper = mount( @@ -131,18 +115,14 @@ describe('Uncommon Process Table Component', () => { const wrapper = mount( @@ -157,18 +137,14 @@ describe('Uncommon Process Table Component', () => { const wrapper = mount( @@ -183,18 +159,14 @@ describe('Uncommon Process Table Component', () => { const wrapper = mount( @@ -208,18 +180,14 @@ describe('Uncommon Process Table Component', () => { const wrapper = mount( @@ -233,18 +201,14 @@ describe('Uncommon Process Table Component', () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx index 31d7fb10edb1c..c7025bb489ae4 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx @@ -9,7 +9,10 @@ import React, { useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { UncommonProcessesEdges, UncommonProcessItem } from '../../../graphql/types'; +import { + HostsUncommonProcessesEdges, + HostsUncommonProcessItem, +} from '../../../../common/search_strategy'; import { State } from '../../../common/store'; import { hostsActions, hostsModel, hostsSelectors } from '../../store'; import { defaultToEmptyTag, getEmptyValue } from '../../../common/components/empty_value'; @@ -21,7 +24,7 @@ import { getRowItemDraggables } from '../../../common/components/tables/helpers' import { HostsType } from '../../store/model'; const tableType = hostsModel.HostsTableType.uncommonProcesses; interface OwnProps { - data: UncommonProcessesEdges[]; + data: HostsUncommonProcessesEdges[]; fakeTotalCount: number; id: string; isInspect: boolean; @@ -33,12 +36,12 @@ interface OwnProps { } export type UncommonProcessTableColumns = [ - Columns, - Columns, - Columns, - Columns, - Columns, - Columns + Columns, + Columns, + Columns, + Columns, + Columns, + Columns ]; type UncommonProcessTableProps = OwnProps & PropsFromRedux; @@ -212,7 +215,7 @@ const getUncommonColumns = (): UncommonProcessTableColumns => [ }, ]; -export const getHostNames = (node: UncommonProcessItem): string[] => { +export const getHostNames = (node: HostsUncommonProcessItem): string[] => { if (node.hosts != null) { return node.hosts .filter((host) => host.name != null && host.name[0] != null) diff --git a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/mock.ts b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/mock.ts index 52b835278634b..56853c1bfaae1 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/mock.ts +++ b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/mock.ts @@ -4,116 +4,115 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UncommonProcessesData } from '../../../graphql/types'; +import { HostsUncommonProcessesStrategyResponse } from '../../../../common/search_strategy'; -export const mockData: { UncommonProcess: UncommonProcessesData } = { - UncommonProcess: { - totalCount: 5, - edges: [ - { - node: { - _id: 'cPsuhGcB0WOhS6qyTKC0', - process: { - title: ['Hello World'], - name: ['elrond.elstc.co'], - }, - hosts: [], - instances: 93, - user: { - id: ['0'], - name: ['root'], - }, +export const mockData: HostsUncommonProcessesStrategyResponse = { + totalCount: 5, + edges: [ + { + node: { + _id: 'cPsuhGcB0WOhS6qyTKC0', + process: { + title: ['Hello World'], + name: ['elrond.elstc.co'], }, - cursor: { - value: '98966fa2013c396155c460d35c0902be', + hosts: [], + instances: 93, + user: { + id: ['0'], + name: ['root'], }, }, - { - node: { - _id: 'cPsuhGcB0WOhS6qyTKC0', - process: { - title: ['Hello World'], - name: ['elrond.elstc.co'], - }, - hosts: [{ id: ['host-id-1'], name: ['hello-world'] }], - instances: 93, - user: { - id: ['0'], - name: ['root'], - }, + cursor: { + value: '98966fa2013c396155c460d35c0902be', + }, + }, + { + node: { + _id: 'cPsuhGcB0WOhS6qyTKC0', + process: { + title: ['Hello World'], + name: ['elrond.elstc.co'], }, - cursor: { - value: '98966fa2013c396155c460d35c0902be', + hosts: [{ id: ['host-id-1'], name: ['hello-world'] }], + instances: 93, + user: { + id: ['0'], + name: ['root'], }, }, - { - node: { - _id: 'KwQDiWcB0WOhS6qyXmrW', - process: { - title: ['Hello World'], - name: ['siem-kibana'], - }, - hosts: [ - { id: ['host-id-1'], name: ['hello-world'] }, - { id: ['host-id-2'], name: ['hello-world-2'] }, - ], - instances: 97, - user: { - id: ['1'], - name: ['Evan'], - }, + cursor: { + value: '98966fa2013c396155c460d35c0902be', + }, + }, + { + node: { + _id: 'KwQDiWcB0WOhS6qyXmrW', + process: { + title: ['Hello World'], + name: ['siem-kibana'], }, - cursor: { - value: 'aa7ca589f1b8220002f2fc61c64cfbf1', + hosts: [ + { id: ['host-id-1'], name: ['hello-world'] }, + { id: ['host-id-2'], name: ['hello-world-2'] }, + ], + instances: 97, + user: { + id: ['1'], + name: ['Evan'], }, }, - { - node: { - _id: 'KwQDiWcB0WOhS6qyXmrW', - process: { - title: ['Hello World'], - name: ['siem-kibana'], - }, - hosts: [{ ip: ['127.0.0.1'] }], - instances: 97, - user: { - id: ['1'], - name: ['Evan'], - }, + cursor: { + value: 'aa7ca589f1b8220002f2fc61c64cfbf1', + }, + }, + { + node: { + _id: 'KwQDiWcB0WOhS6qyXmrW', + process: { + title: ['Hello World'], + name: ['siem-kibana'], }, - cursor: { - value: 'aa7ca589f1b8220002f2fc61c64cfbf1', + hosts: [{ ip: ['127.0.0.1'] }], + instances: 97, + user: { + id: ['1'], + name: ['Evan'], }, }, - { - node: { - _id: 'KwQDiWcB0WOhS6qyXmrW', - process: { - title: ['Hello World'], - name: ['siem-kibana'], - }, - hosts: [ - { ip: ['127.0.0.1'] }, - { id: ['host-id-1'], name: ['hello-world'] }, - { ip: ['127.0.0.1'] }, - { id: ['host-id-2'], name: ['hello-world-2'] }, - { ip: ['127.0.0.1'] }, - ], - instances: 97, - user: { - id: ['1'], - name: ['Evan'], - }, + cursor: { + value: 'aa7ca589f1b8220002f2fc61c64cfbf1', + }, + }, + { + node: { + _id: 'KwQDiWcB0WOhS6qyXmrW', + process: { + title: ['Hello World'], + name: ['siem-kibana'], }, - cursor: { - value: 'aa7ca589f1b8220002f2fc61c64cfbf1', + hosts: [ + { ip: ['127.0.0.1'] }, + { id: ['host-id-1'], name: ['hello-world'] }, + { ip: ['127.0.0.1'] }, + { id: ['host-id-2'], name: ['hello-world-2'] }, + { ip: ['127.0.0.1'] }, + ], + instances: 97, + user: { + id: ['1'], + name: ['Evan'], }, }, - ], - pageInfo: { - activePage: 1, - fakeTotalCount: 50, - showMorePagesIndicator: true, + cursor: { + value: 'aa7ca589f1b8220002f2fc61c64cfbf1', + }, }, + ], + pageInfo: { + activePage: 1, + fakeTotalCount: 50, + showMorePagesIndicator: true, }, + rawResponse: {} as HostsUncommonProcessesStrategyResponse['rawResponse'], }; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.gql_query.ts b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.gql_query.ts deleted file mode 100644 index d984de020faa1..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.gql_query.ts +++ /dev/null @@ -1,59 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const uncommonProcessesQuery = gql` - query GetUncommonProcessesQuery( - $sourceId: ID! - $timerange: TimerangeInput! - $pagination: PaginationInputPaginated! - $filterQuery: String - $defaultIndex: [String!]! - $inspect: Boolean! - ) { - source(id: $sourceId) { - id - UncommonProcesses( - timerange: $timerange - pagination: $pagination - filterQuery: $filterQuery - defaultIndex: $defaultIndex - ) { - totalCount - edges { - node { - _id - instances - process { - args - name - } - user { - id - name - } - hosts { - name - } - } - cursor { - value - } - } - pageInfo { - activePage - fakeTotalCount - showMorePagesIndicator - } - inspect @include(if: $inspect) { - dsl - response - } - } - } - } -`; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx index ae4ea83f88725..e28a808378dd7 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx @@ -16,19 +16,20 @@ import { } from '../../../../../../../src/plugins/data/common'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { PageInfoPaginated, UncommonProcessesEdges } from '../../../graphql/types'; import { inputsModel, State } from '../../../common/store'; import { useKibana } from '../../../common/lib/kibana'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; import { createFilter } from '../../../common/containers/helpers'; - import { hostsModel, hostsSelectors } from '../../store'; import { - HostUncommonProcessesRequestOptions, - HostUncommonProcessesStrategyResponse, -} from '../../../../common/search_strategy/security_solution/hosts/uncommon_processes'; -import { HostsQueries } from '../../../../common/search_strategy/security_solution/hosts'; -import { DocValueFields, SortField } from '../../../../common/search_strategy'; + DocValueFields, + SortField, + PageInfoPaginated, + HostsUncommonProcessesEdges, + HostsQueries, + HostsUncommonProcessesRequestOptions, + HostsUncommonProcessesStrategyResponse, +} from '../../../../common/search_strategy'; import * as i18n from './translations'; import { ESTermQuery } from '../../../../common/typed_json'; @@ -45,7 +46,7 @@ export interface UncommonProcessesArgs { pageInfo: PageInfoPaginated; refetch: inputsModel.Refetch; totalCount: number; - uncommonProcesses: UncommonProcessesEdges[]; + uncommonProcesses: HostsUncommonProcessesEdges[]; } interface UseUncommonProcesses { @@ -75,7 +76,7 @@ export const useUncommonProcesses = ({ const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [uncommonProcessesRequest, setUncommonProcessesRequest] = useState< - HostUncommonProcessesRequestOptions + HostsUncommonProcessesRequestOptions >({ defaultIndex, docValueFields: docValueFields ?? [], @@ -123,14 +124,14 @@ export const useUncommonProcesses = ({ ); const uncommonProcessesSearch = useCallback( - (request: HostUncommonProcessesRequestOptions) => { + (request: HostsUncommonProcessesRequestOptions) => { let didCancel = false; const asyncSearch = async () => { abortCtrl.current = new AbortController(); setLoading(true); const searchSubscription$ = data.search - .search( + .search( request, { strategy: 'securitySolutionSearchStrategy', diff --git a/x-pack/plugins/security_solution/server/graphql/index.ts b/x-pack/plugins/security_solution/server/graphql/index.ts index 959aa4549d43f..e949150c47c6c 100644 --- a/x-pack/plugins/security_solution/server/graphql/index.ts +++ b/x-pack/plugins/security_solution/server/graphql/index.ts @@ -26,7 +26,6 @@ import { toNumberSchema } from './scalar_to_number_array'; import { sourceStatusSchema } from './source_status'; import { sourcesSchema } from './sources'; import { timelineSchema } from './timeline'; -import { uncommonProcessesSchema } from './uncommon_processes'; import { whoAmISchema } from './who_am_i'; import { matrixHistogramSchema } from './matrix_histogram'; export const schemas = [ @@ -52,6 +51,5 @@ export const schemas = [ sourceStatusSchema, sharedSchema, timelineSchema, - uncommonProcessesSchema, whoAmISchema, ]; diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index ed3abd25df882..5887feb63c2a1 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -560,8 +560,6 @@ export interface Source { OverviewNetwork?: Maybe; OverviewHost?: Maybe; - /** Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified */ - UncommonProcesses: UncommonProcessesData; /** Just a simple example to get the app name */ whoAmI?: Maybe; } @@ -1918,34 +1916,6 @@ export interface OverviewHostData { inspect?: Maybe; } -export interface UncommonProcessesData { - edges: UncommonProcessesEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe; -} - -export interface UncommonProcessesEdges { - node: UncommonProcessItem; - - cursor: CursorType; -} - -export interface UncommonProcessItem { - _id: string; - - instances: number; - - process: ProcessEcsFields; - - hosts: HostEcsFields[]; - - user?: Maybe; -} - export interface SayMyName { /** The id of the source */ appName: string; @@ -2533,15 +2503,6 @@ export interface OverviewHostSourceArgs { defaultIndex: string[]; } -export interface UncommonProcessesSourceArgs { - timerange: TimerangeInput; - - pagination: PaginationInputPaginated; - - filterQuery?: Maybe; - - defaultIndex: string[]; -} export interface IndicesExistSourceStatusArgs { defaultIndex: string[]; } @@ -2982,8 +2943,6 @@ export namespace SourceResolvers { OverviewNetwork?: OverviewNetworkResolver, TypeParent, TContext>; OverviewHost?: OverviewHostResolver, TypeParent, TContext>; - /** Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified */ - UncommonProcesses?: UncommonProcessesResolver; /** Just a simple example to get the app name */ whoAmI?: WhoAmIResolver, TypeParent, TContext>; } @@ -3365,21 +3324,6 @@ export namespace SourceResolvers { defaultIndex: string[]; } - export type UncommonProcessesResolver< - R = UncommonProcessesData, - Parent = Source, - TContext = SiemContext - > = Resolver; - export interface UncommonProcessesArgs { - timerange: TimerangeInput; - - pagination: PaginationInputPaginated; - - filterQuery?: Maybe; - - defaultIndex: string[]; - } - export type WhoAmIResolver< R = Maybe, Parent = Source, @@ -7936,98 +7880,6 @@ export namespace OverviewHostDataResolvers { > = Resolver; } -export namespace UncommonProcessesDataResolvers { - export interface Resolvers { - edges?: EdgesResolver; - - totalCount?: TotalCountResolver; - - pageInfo?: PageInfoResolver; - - inspect?: InspectResolver, TypeParent, TContext>; - } - - export type EdgesResolver< - R = UncommonProcessesEdges[], - Parent = UncommonProcessesData, - TContext = SiemContext - > = Resolver; - export type TotalCountResolver< - R = number, - Parent = UncommonProcessesData, - TContext = SiemContext - > = Resolver; - export type PageInfoResolver< - R = PageInfoPaginated, - Parent = UncommonProcessesData, - TContext = SiemContext - > = Resolver; - export type InspectResolver< - R = Maybe, - Parent = UncommonProcessesData, - TContext = SiemContext - > = Resolver; -} - -export namespace UncommonProcessesEdgesResolvers { - export interface Resolvers { - node?: NodeResolver; - - cursor?: CursorResolver; - } - - export type NodeResolver< - R = UncommonProcessItem, - Parent = UncommonProcessesEdges, - TContext = SiemContext - > = Resolver; - export type CursorResolver< - R = CursorType, - Parent = UncommonProcessesEdges, - TContext = SiemContext - > = Resolver; -} - -export namespace UncommonProcessItemResolvers { - export interface Resolvers { - _id?: _IdResolver; - - instances?: InstancesResolver; - - process?: ProcessResolver; - - hosts?: HostsResolver; - - user?: UserResolver, TypeParent, TContext>; - } - - export type _IdResolver< - R = string, - Parent = UncommonProcessItem, - TContext = SiemContext - > = Resolver; - export type InstancesResolver< - R = number, - Parent = UncommonProcessItem, - TContext = SiemContext - > = Resolver; - export type ProcessResolver< - R = ProcessEcsFields, - Parent = UncommonProcessItem, - TContext = SiemContext - > = Resolver; - export type HostsResolver< - R = HostEcsFields[], - Parent = UncommonProcessItem, - TContext = SiemContext - > = Resolver; - export type UserResolver< - R = Maybe, - Parent = UncommonProcessItem, - TContext = SiemContext - > = Resolver; -} - export namespace SayMyNameResolvers { export interface Resolvers { /** The id of the source */ @@ -9308,9 +9160,6 @@ export type IResolvers = { NetworkHttpItem?: NetworkHttpItemResolvers.Resolvers; OverviewNetworkData?: OverviewNetworkDataResolvers.Resolvers; OverviewHostData?: OverviewHostDataResolvers.Resolvers; - UncommonProcessesData?: UncommonProcessesDataResolvers.Resolvers; - UncommonProcessesEdges?: UncommonProcessesEdgesResolvers.Resolvers; - UncommonProcessItem?: UncommonProcessItemResolvers.Resolvers; SayMyName?: SayMyNameResolvers.Resolvers; TimelineResult?: TimelineResultResolvers.Resolvers; ColumnHeaderResult?: ColumnHeaderResultResolvers.Resolvers; diff --git a/x-pack/plugins/security_solution/server/graphql/uncommon_processes/index.ts b/x-pack/plugins/security_solution/server/graphql/uncommon_processes/index.ts deleted file mode 100644 index d0da0efd8a560..0000000000000 --- a/x-pack/plugins/security_solution/server/graphql/uncommon_processes/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { createUncommonProcessesResolvers } from './resolvers'; -export { uncommonProcessesSchema } from './schema.gql'; diff --git a/x-pack/plugins/security_solution/server/graphql/uncommon_processes/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/uncommon_processes/resolvers.ts deleted file mode 100644 index 03d3c3d1a1fe4..0000000000000 --- a/x-pack/plugins/security_solution/server/graphql/uncommon_processes/resolvers.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SourceResolvers } from '../../graphql/types'; -import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; -import { UncommonProcesses } from '../../lib/uncommon_processes'; -import { createOptionsPaginated } from '../../utils/build_query/create_options'; -import { QuerySourceResolver } from '../sources/resolvers'; - -type QueryUncommonProcessesResolver = ChildResolverOf< - AppResolverOf, - QuerySourceResolver ->; - -export interface UncommonProcessesResolversDeps { - uncommonProcesses: UncommonProcesses; -} - -export const createUncommonProcessesResolvers = ( - libs: UncommonProcessesResolversDeps -): { - Source: { - UncommonProcesses: QueryUncommonProcessesResolver; - }; -} => ({ - Source: { - async UncommonProcesses(source, args, { req }, info) { - const options = createOptionsPaginated(source, args, info); - return libs.uncommonProcesses.getUncommonProcesses(req, options); - }, - }, -}); diff --git a/x-pack/plugins/security_solution/server/graphql/uncommon_processes/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/uncommon_processes/schema.gql.ts deleted file mode 100644 index 36a3da6779172..0000000000000 --- a/x-pack/plugins/security_solution/server/graphql/uncommon_processes/schema.gql.ts +++ /dev/null @@ -1,39 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const uncommonProcessesSchema = gql` - type UncommonProcessItem { - _id: String! - instances: Float! - process: ProcessEcsFields! - hosts: [HostEcsFields!]! - user: UserEcsFields - } - - type UncommonProcessesEdges { - node: UncommonProcessItem! - cursor: CursorType! - } - - type UncommonProcessesData { - edges: [UncommonProcessesEdges!]! - totalCount: Float! - pageInfo: PageInfoPaginated! - inspect: Inspect - } - - extend type Source { - "Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified" - UncommonProcesses( - timerange: TimerangeInput! - pagination: PaginationInputPaginated! - filterQuery: String - defaultIndex: [String!]! - ): UncommonProcessesData! - } -`; diff --git a/x-pack/plugins/security_solution/server/init_server.ts b/x-pack/plugins/security_solution/server/init_server.ts index 2ef42eaee4b98..7cb2127a3d9d7 100644 --- a/x-pack/plugins/security_solution/server/init_server.ts +++ b/x-pack/plugins/security_solution/server/init_server.ts @@ -25,7 +25,6 @@ import { createScalarToNumberArrayValueResolvers } from './graphql/scalar_to_num import { createSourceStatusResolvers } from './graphql/source_status'; import { createSourcesResolvers } from './graphql/sources'; import { createTimelineResolvers } from './graphql/timeline'; -import { createUncommonProcessesResolvers } from './graphql/uncommon_processes'; import { createWhoAmIResolvers } from './graphql/who_am_i'; import { AppBackendLibs } from './lib/types'; import { createMatrixHistogramResolvers } from './graphql/matrix_histogram'; @@ -54,7 +53,6 @@ export const initServer = (libs: AppBackendLibs) => { createSourcesResolvers(libs) as IResolvers, createSourceStatusResolvers(libs) as IResolvers, createTimelineResolvers(libs) as IResolvers, - createUncommonProcessesResolvers(libs) as IResolvers, createWhoAmIResolvers() as IResolvers, createKpiHostsResolvers(libs) as IResolvers, ], diff --git a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts index bab00e33e3378..430ada93b4514 100644 --- a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts +++ b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts @@ -26,7 +26,6 @@ import { ElasticsearchOverviewAdapter } from '../overview/elasticsearch_adapter' import { ElasticsearchSourceStatusAdapter, SourceStatus } from '../source_status'; import { ConfigurationSourcesAdapter, Sources } from '../sources'; import { AppBackendLibs, AppDomainLibs } from '../types'; -import { ElasticsearchUncommonProcessesAdapter, UncommonProcesses } from '../uncommon_processes'; import * as note from '../note/saved_object'; import * as pinnedEvent from '../pinned_event/saved_object'; import * as timeline from '../timeline/saved_object'; @@ -54,7 +53,6 @@ export function compose( matrixHistogram: new MatrixHistogram(new ElasticsearchMatrixHistogramAdapter(framework)), network: new Network(new ElasticsearchNetworkAdapter(framework)), overview: new Overview(new ElasticsearchOverviewAdapter(framework)), - uncommonProcesses: new UncommonProcesses(new ElasticsearchUncommonProcessesAdapter(framework)), }; const libs: AppBackendLibs = { diff --git a/x-pack/plugins/security_solution/server/lib/types.ts b/x-pack/plugins/security_solution/server/lib/types.ts index 4f70e3aa8652a..87e755360285f 100644 --- a/x-pack/plugins/security_solution/server/lib/types.ts +++ b/x-pack/plugins/security_solution/server/lib/types.ts @@ -20,7 +20,6 @@ import { Network } from './network'; import { Overview } from './overview'; import { SourceStatus } from './source_status'; import { Sources } from './sources'; -import { UncommonProcesses } from './uncommon_processes'; import { Note } from './note/saved_object'; import { PinnedEvent } from './pinned_event/saved_object'; import { Timeline } from './timeline/saved_object'; @@ -38,7 +37,6 @@ export interface AppDomainLibs { network: Network; kpiNetwork: KpiNetwork; overview: Overview; - uncommonProcesses: UncommonProcesses; kpiHosts: KpiHosts; } diff --git a/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts b/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts deleted file mode 100644 index 2a15f1fe074f8..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts +++ /dev/null @@ -1,265 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import { UncommonProcessesEdges } from '../../graphql/types'; -import { processFieldsMap } from '../ecs_fields'; - -import { formatUncommonProcessesData, getHosts } from './elasticsearch_adapter'; -import { UncommonProcessBucket, UncommonProcessHit } from './types'; - -describe('elasticsearch_adapter', () => { - describe('#getHosts', () => { - const bucket1: UncommonProcessBucket = { - key: '123', - hosts: { - buckets: [ - { - key: '123', - host: { - hits: { - total: 0, - max_score: 0, - hits: [ - { - _index: 'hit-1', - _type: 'type-1', - _id: 'id-1', - _score: 0, - _source: { - host: { - name: ['host-1'], - id: ['host-id-1'], - }, - }, - }, - ], - }, - }, - }, - ], - }, - process: { - hits: { - total: { - value: 1, - relation: 'eq', - }, - max_score: 5, - hits: [], - }, - }, - }; - const bucket2: UncommonProcessBucket = { - key: '345', - hosts: { - buckets: [ - { - key: '123', - host: { - hits: { - total: 0, - max_score: 0, - hits: [ - { - _index: 'hit-1', - _type: 'type-1', - _id: 'id-1', - _score: 0, - _source: { - host: { - name: ['host-1'], - id: ['host-id-1'], - }, - }, - }, - ], - }, - }, - }, - { - key: '345', - host: { - hits: { - total: 0, - max_score: 0, - hits: [ - { - _index: 'hit-2', - _type: 'type-2', - _id: 'id-2', - _score: 0, - _source: { - host: { - name: ['host-2'], - id: ['host-id-2'], - }, - }, - }, - ], - }, - }, - }, - ], - }, - process: { - hits: { - total: { - value: 1, - relation: 'eq', - }, - max_score: 5, - hits: [], - }, - }, - }; - const bucket3: UncommonProcessBucket = { - key: '789', - hosts: { - buckets: [ - { - key: '789', - host: { - hits: { - total: 0, - max_score: 0, - hits: [ - { - _index: 'hit-9', - _type: 'type-9', - _id: 'id-9', - _score: 0, - _source: { - // @ts-expect-error ts doesn't like seeing the object written this way, but sometimes this is the data we get! - 'host.id': ['host-id-9'], - 'host.name': ['host-9'], - }, - }, - ], - }, - }, - }, - ], - }, - process: { - hits: { - total: { - value: 1, - relation: 'eq', - }, - max_score: 5, - hits: [], - }, - }, - }; - - test('will return a single host correctly', () => { - const hosts = getHosts(bucket1.hosts.buckets); - expect(hosts).toEqual([{ id: ['123'], name: ['host-1'] }]); - }); - - test('will return two hosts correctly', () => { - const hosts = getHosts(bucket2.hosts.buckets); - expect(hosts).toEqual([ - { id: ['123'], name: ['host-1'] }, - { id: ['345'], name: ['host-2'] }, - ]); - }); - - test('will return a dot notation host', () => { - const hosts = getHosts(bucket3.hosts.buckets); - expect(hosts).toEqual([{ id: ['789'], name: ['host-9'] }]); - }); - - test('will return no hosts when given an empty array', () => { - const hosts = getHosts([]); - expect(hosts).toEqual([]); - }); - }); - - describe('#formatUncommonProcessesData', () => { - const hit: UncommonProcessHit = { - _index: 'index-123', - _type: 'type-123', - _id: 'id-123', - _score: 10, - total: { - value: 100, - relation: 'eq', - }, - host: [ - { id: ['host-id-1'], name: ['host-name-1'] }, - { id: ['host-id-1'], name: ['host-name-1'] }, - ], - _source: { - '@timestamp': 'time', - process: { - name: ['process-1'], - title: ['title-1'], - }, - }, - cursor: 'cursor-1', - sort: [0], - }; - - test('it formats a uncommon process data with a source of name correctly', () => { - const fields: readonly string[] = ['process.name']; - const data = formatUncommonProcessesData(fields, hit, processFieldsMap); - const expected: UncommonProcessesEdges = { - cursor: { tiebreaker: null, value: 'cursor-1' }, - node: { - _id: 'id-123', - hosts: [ - { id: ['host-id-1'], name: ['host-name-1'] }, - { id: ['host-id-1'], name: ['host-name-1'] }, - ], - process: { - name: ['process-1'], - }, - instances: 100, - }, - }; - expect(data).toEqual(expected); - }); - - test('it formats a uncommon process data with a source of name and title correctly', () => { - const fields: readonly string[] = ['process.name', 'process.title']; - const data = formatUncommonProcessesData(fields, hit, processFieldsMap); - const expected: UncommonProcessesEdges = { - cursor: { tiebreaker: null, value: 'cursor-1' }, - node: { - _id: 'id-123', - hosts: [ - { id: ['host-id-1'], name: ['host-name-1'] }, - { id: ['host-id-1'], name: ['host-name-1'] }, - ], - instances: 100, - process: { - name: ['process-1'], - title: ['title-1'], - }, - }, - }; - expect(data).toEqual(expected); - }); - - test('it formats a uncommon process data without any data if fields is empty', () => { - const fields: readonly string[] = []; - const data = formatUncommonProcessesData(fields, hit, processFieldsMap); - const expected: UncommonProcessesEdges = { - cursor: { - tiebreaker: null, - value: '', - }, - node: { - _id: '', - hosts: [], - instances: 0, - process: {}, - }, - }; - expect(data).toEqual(expected); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.ts deleted file mode 100644 index 046823da7cb85..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.ts +++ /dev/null @@ -1,118 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get, getOr } from 'lodash/fp'; - -import { UncommonProcessesData, UncommonProcessesEdges } from '../../graphql/types'; -import { mergeFieldsWithHit, inspectStringifyObject } from '../../utils/build_query'; -import { processFieldsMap, userFieldsMap } from '../ecs_fields'; -import { FrameworkAdapter, FrameworkRequest, RequestOptionsPaginated } from '../framework'; -import { HostHits, TermAggregation } from '../types'; -import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; -import { buildQuery } from './query.dsl'; -import { - UncommonProcessBucket, - UncommonProcessData, - UncommonProcessesAdapter, - UncommonProcessHit, -} from './types'; - -export class ElasticsearchUncommonProcessesAdapter implements UncommonProcessesAdapter { - constructor(private readonly framework: FrameworkAdapter) {} - - public async getUncommonProcesses( - request: FrameworkRequest, - options: RequestOptionsPaginated - ): Promise { - if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { - throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); - } - const dsl = buildQuery(options); - const response = await this.framework.callWithRequest( - request, - 'search', - dsl - ); - const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; - const totalCount = getOr(0, 'aggregations.process_count.value', response); - const buckets = getOr([], 'aggregations.group_by_process.buckets', response); - const hits = getHits(buckets); - - const uncommonProcessesEdges = hits.map((hit) => - formatUncommonProcessesData(options.fields, hit, { ...processFieldsMap, ...userFieldsMap }) - ); - - const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; - const edges = uncommonProcessesEdges.splice(cursorStart, querySize - cursorStart); - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - - const showMorePagesIndicator = totalCount > fakeTotalCount; - return { - edges, - inspect, - pageInfo: { - activePage: activePage ? activePage : 0, - fakeTotalCount, - showMorePagesIndicator, - }, - totalCount, - }; - } -} - -export const getHits = (buckets: readonly UncommonProcessBucket[]): readonly UncommonProcessHit[] => - buckets.map((bucket: Readonly) => ({ - _id: bucket.process.hits.hits[0]._id, - _index: bucket.process.hits.hits[0]._index, - _type: bucket.process.hits.hits[0]._type, - _score: bucket.process.hits.hits[0]._score, - _source: bucket.process.hits.hits[0]._source, - sort: bucket.process.hits.hits[0].sort, - cursor: bucket.process.hits.hits[0].cursor, - total: bucket.process.hits.total, - host: getHosts(bucket.hosts.buckets), - })); - -export const getHosts = (buckets: ReadonlyArray<{ key: string; host: HostHits }>) => - buckets.map((bucket) => { - const source = get('host.hits.hits[0]._source', bucket); - return { - id: [bucket.key], - name: get('host.name', source), - }; - }); - -export const formatUncommonProcessesData = ( - fields: readonly string[], - hit: UncommonProcessHit, - fieldMap: Readonly> -): UncommonProcessesEdges => - fields.reduce( - (flattenedFields, fieldName) => { - flattenedFields.node._id = hit._id; - flattenedFields.node.instances = getOr(0, 'total.value', hit); - flattenedFields.node.hosts = hit.host; - if (hit.cursor) { - flattenedFields.cursor.value = hit.cursor; - } - return mergeFieldsWithHit(fieldName, flattenedFields, fieldMap, hit); - }, - { - node: { - _id: '', - instances: 0, - process: {}, - hosts: [], - }, - cursor: { - value: '', - tiebreaker: null, - }, - } - ); diff --git a/x-pack/plugins/security_solution/server/lib/uncommon_processes/index.ts b/x-pack/plugins/security_solution/server/lib/uncommon_processes/index.ts deleted file mode 100644 index 0ba0e90f391e1..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/uncommon_processes/index.ts +++ /dev/null @@ -1,21 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { UncommonProcessesData } from '../../graphql/types'; -import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; -export * from './elasticsearch_adapter'; -import { UncommonProcessesAdapter } from './types'; - -export class UncommonProcesses { - constructor(private readonly adapter: UncommonProcessesAdapter) {} - - public async getUncommonProcesses( - req: FrameworkRequest, - options: RequestOptionsPaginated - ): Promise { - return this.adapter.getUncommonProcesses(req, options); - } -} diff --git a/x-pack/plugins/security_solution/server/lib/uncommon_processes/query.dsl.ts b/x-pack/plugins/security_solution/server/lib/uncommon_processes/query.dsl.ts deleted file mode 100644 index 4563c769cdc31..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/uncommon_processes/query.dsl.ts +++ /dev/null @@ -1,222 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { createQueryFilterClauses } from '../../utils/build_query'; -import { reduceFields } from '../../utils/build_query/reduce_fields'; -import { hostFieldsMap, processFieldsMap, userFieldsMap } from '../ecs_fields'; -import { RequestOptionsPaginated } from '../framework'; - -export const buildQuery = ({ - defaultIndex, - fields, - filterQuery, - pagination: { querySize }, - sourceConfiguration: { - fields: { timestamp }, - }, - timerange: { from, to }, -}: RequestOptionsPaginated) => { - const processUserFields = reduceFields(fields, { ...processFieldsMap, ...userFieldsMap }); - const hostFields = reduceFields(fields, hostFieldsMap); - const filter = [ - ...createQueryFilterClauses(filterQuery), - { - range: { - [timestamp]: { - gte: from, - lte: to, - format: 'strict_date_optional_time', - }, - }, - }, - ]; - - const agg = { - process_count: { - cardinality: { - field: 'process.name', - }, - }, - }; - - const dslQuery = { - allowNoIndices: true, - index: defaultIndex, - ignoreUnavailable: true, - body: { - aggregations: { - ...agg, - group_by_process: { - terms: { - size: querySize, - field: 'process.name', - order: [ - { - host_count: 'asc', - }, - { - _count: 'asc', - }, - { - _key: 'asc', - }, - ], - }, - aggregations: { - process: { - top_hits: { - size: 1, - sort: [{ '@timestamp': { order: 'desc' } }], - _source: processUserFields, - }, - }, - host_count: { - cardinality: { - field: 'host.name', - }, - }, - hosts: { - terms: { - field: 'host.name', - }, - aggregations: { - host: { - top_hits: { - size: 1, - _source: hostFields, - }, - }, - }, - }, - }, - }, - }, - query: { - bool: { - should: [ - { - bool: { - filter: [ - { - term: { - 'agent.type': 'auditbeat', - }, - }, - { - term: { - 'event.module': 'auditd', - }, - }, - { - term: { - 'event.action': 'executed', - }, - }, - ], - }, - }, - { - bool: { - filter: [ - { - term: { - 'agent.type': 'auditbeat', - }, - }, - { - term: { - 'event.module': 'system', - }, - }, - { - term: { - 'event.dataset': 'process', - }, - }, - { - term: { - 'event.action': 'process_started', - }, - }, - ], - }, - }, - { - bool: { - filter: [ - { - term: { - 'agent.type': 'winlogbeat', - }, - }, - { - term: { - 'event.code': '4688', - }, - }, - ], - }, - }, - { - bool: { - filter: [ - { - term: { - 'winlog.event_id': 1, - }, - }, - { - term: { - 'winlog.channel': 'Microsoft-Windows-Sysmon/Operational', - }, - }, - ], - }, - }, - { - bool: { - filter: [ - { - term: { - 'event.type': 'process_start', - }, - }, - { - term: { - 'event.category': 'process', - }, - }, - ], - }, - }, - { - bool: { - filter: [ - { - term: { - 'event.category': 'process', - }, - }, - { - term: { - 'event.type': 'start', - }, - }, - ], - }, - }, - ], - minimum_should_match: 1, - filter, - }, - }, - }, - size: 0, - track_total_hits: false, - }; - - return dslQuery; -}; diff --git a/x-pack/plugins/security_solution/server/lib/uncommon_processes/types.ts b/x-pack/plugins/security_solution/server/lib/uncommon_processes/types.ts deleted file mode 100644 index dc60de5963a18..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/uncommon_processes/types.ts +++ /dev/null @@ -1,54 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ProcessEcsFields, UncommonProcessesData } from '../../graphql/types'; -import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; -import { Hit, Hits, HostHits, SearchHit, TotalHit } from '../types'; - -export interface UncommonProcessesAdapter { - getUncommonProcesses( - req: FrameworkRequest, - options: RequestOptionsPaginated - ): Promise; -} - -type StringOrNumber = string | number; -export interface UncommonProcessHit extends Hit { - total: TotalHit; - host: Array<{ - id: string[] | string | null | undefined; - name: string[] | string | null | undefined; - }>; - _source: { - '@timestamp': string; - process: ProcessEcsFields; - }; - cursor: string; - sort: StringOrNumber[]; -} - -export type ProcessHits = Hits; - -export interface UncommonProcessBucket { - key: string; - hosts: { - buckets: Array<{ key: string; host: HostHits }>; - }; - process: ProcessHits; -} - -export interface UncommonProcessData extends SearchHit { - sort: string[]; - aggregations: { - process_count: { - value: number; - }; - group_by_process: { - after_key: string; - buckets: UncommonProcessBucket[]; - }; - }; -} diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.test.ts index 096ca570ae852..a6f44c78e5cc4 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.test.ts @@ -7,8 +7,8 @@ import { processFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { - UncommonProcessesEdges, - UncommonProcessHit, + HostsUncommonProcessesEdges, + HostsUncommonProcessHit, } from '../../../../../../common/search_strategy'; import { formatUncommonProcessesData, getHosts, UncommonProcessBucket } from './helpers'; @@ -183,7 +183,7 @@ describe('helpers', () => { }); describe('#formatUncommonProcessesData', () => { - const hit: UncommonProcessHit = { + const hit: HostsUncommonProcessHit = { _index: 'index-123', _type: 'type-123', _id: 'id-123', @@ -210,7 +210,7 @@ describe('helpers', () => { test('it formats a uncommon process data with a source of name correctly', () => { const fields: readonly string[] = ['process.name']; const data = formatUncommonProcessesData(fields, hit, processFieldsMap); - const expected: UncommonProcessesEdges = { + const expected: HostsUncommonProcessesEdges = { cursor: { tiebreaker: null, value: 'cursor-1' }, node: { _id: 'id-123', @@ -230,7 +230,7 @@ describe('helpers', () => { test('it formats a uncommon process data with a source of name and title correctly', () => { const fields: readonly string[] = ['process.name', 'process.title']; const data = formatUncommonProcessesData(fields, hit, processFieldsMap); - const expected: UncommonProcessesEdges = { + const expected: HostsUncommonProcessesEdges = { cursor: { tiebreaker: null, value: 'cursor-1' }, node: { _id: 'id-123', @@ -251,7 +251,7 @@ describe('helpers', () => { test('it formats a uncommon process data without any data if fields is empty', () => { const fields: readonly string[] = []; const data = formatUncommonProcessesData(fields, hit, processFieldsMap); - const expected: UncommonProcessesEdges = { + const expected: HostsUncommonProcessesEdges = { cursor: { tiebreaker: null, value: '', diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts index 5c3d76175b7e4..20b3f5b05bc87 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts @@ -9,8 +9,8 @@ import { set } from '@elastic/safer-lodash-set/fp'; import { mergeFieldsWithHit } from '../../../../../utils/build_query'; import { ProcessHits, - UncommonProcessesEdges, - UncommonProcessHit, + HostsUncommonProcessesEdges, + HostsUncommonProcessHit, } from '../../../../../../common/search_strategy/security_solution/hosts/uncommon_processes'; import { toArray } from '../../../../helpers/to_array'; import { HostHits } from '../../../../../../common/search_strategy'; @@ -25,7 +25,9 @@ export const uncommonProcessesFields = [ 'hosts.name', ]; -export const getHits = (buckets: readonly UncommonProcessBucket[]): readonly UncommonProcessHit[] => +export const getHits = ( + buckets: readonly UncommonProcessBucket[] +): readonly HostsUncommonProcessHit[] => buckets.map((bucket: Readonly) => ({ _id: bucket.process.hits.hits[0]._id, _index: bucket.process.hits.hits[0]._index, @@ -57,10 +59,10 @@ export const getHosts = (buckets: ReadonlyArray<{ key: string; host: HostHits }> export const formatUncommonProcessesData = ( fields: readonly string[], - hit: UncommonProcessHit, + hit: HostsUncommonProcessHit, fieldMap: Readonly> -): UncommonProcessesEdges => - fields.reduce( +): HostsUncommonProcessesEdges => + fields.reduce( (flattenedFields, fieldName) => { flattenedFields.node._id = hit._id; flattenedFields.node.instances = getOr(0, 'total.value', hit); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.test.ts index a5fa9b459d1bf..5016c8cc38ce4 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.test.ts @@ -6,7 +6,7 @@ import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; -import { HostUncommonProcessesRequestOptions } from '../../../../../../common/search_strategy/security_solution'; +import { HostsUncommonProcessesRequestOptions } from '../../../../../../common/search_strategy/security_solution'; import * as buildQuery from './dsl/query.dsl'; import { uncommonProcesses } from '.'; import { @@ -35,7 +35,7 @@ describe('uncommonProcesses search strategy', () => { ...mockOptions.pagination, querySize: DEFAULT_MAX_TABLE_QUERY_SIZE, }, - } as HostUncommonProcessesRequestOptions; + } as HostsUncommonProcessesRequestOptions; expect(() => { uncommonProcesses.buildDsl(overSizeOptions); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts index 5682e63b50ed0..add2cdb76628a 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts @@ -12,8 +12,8 @@ import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants import { HostsQueries } from '../../../../../../common/search_strategy/security_solution'; import { processFieldsMap, userFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { - HostUncommonProcessesRequestOptions, - HostUncommonProcessesStrategyResponse, + HostsUncommonProcessesRequestOptions, + HostsUncommonProcessesStrategyResponse, } from '../../../../../../common/search_strategy/security_solution/hosts/uncommon_processes'; import { inspectStringifyObject } from '../../../../../utils/build_query'; @@ -23,16 +23,16 @@ import { buildQuery } from './dsl/query.dsl'; import { formatUncommonProcessesData, getHits, uncommonProcessesFields } from './helpers'; export const uncommonProcesses: SecuritySolutionFactory = { - buildDsl: (options: HostUncommonProcessesRequestOptions) => { + buildDsl: (options: HostsUncommonProcessesRequestOptions) => { if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); } return buildQuery(options); }, parse: async ( - options: HostUncommonProcessesRequestOptions, + options: HostsUncommonProcessesRequestOptions, response: IEsSearchResponse - ): Promise => { + ): Promise => { const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; const totalCount = getOr(0, 'aggregations.process_count.value', response.rawResponse); const buckets = getOr([], 'aggregations.group_by_process.buckets', response.rawResponse); diff --git a/x-pack/test/api_integration/apis/security_solution/index.js b/x-pack/test/api_integration/apis/security_solution/index.js index e4204ae295653..16a38c0fafbca 100644 --- a/x-pack/test/api_integration/apis/security_solution/index.js +++ b/x-pack/test/api_integration/apis/security_solution/index.js @@ -20,7 +20,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./overview_network')); loadTestFile(require.resolve('./timeline')); loadTestFile(require.resolve('./timeline_details')); - loadTestFile(require.resolve('./uncommon_processes')); + // loadTestFile(require.resolve('./uncommon_processes')); loadTestFile(require.resolve('./users')); // loadTestFile(require.resolve('./tls')); loadTestFile(require.resolve('./feature_controls')); diff --git a/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts b/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts index f1e064bcc37bb..1ed9a03ecf87e 100644 --- a/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts +++ b/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts @@ -6,7 +6,9 @@ import expect from '@kbn/expect'; +// @ts-expect-error import { uncommonProcessesQuery } from '../../../../plugins/security_solution/public/hosts/containers/uncommon_processes/index.gql_query'; +// @ts-expect-error import { GetUncommonProcessesQuery } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; From c07a512e4647a40d8e891eb24f5912783b561fba Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Wed, 23 Sep 2020 11:48:54 -0400 Subject: [PATCH 50/92] [Monitoring] Design/UI improvements (#76946) * UI tweaks * Add more page titles * Respect pagination settings * Update snapshot * Fix loc issues * Update node listing * Fix tests * Update icon * Update jobs label * More label changes * Fix tests * Fix tests * PR feedback * Improve responsive design here * PR feedback * Fix tests * Fix test and i18n * Remove unused translations * Fix tests * Tweaks --- .../monitoring/public/angular/app_modules.ts | 6 - .../components/apm/instance/instance.js | 14 +- .../components/apm/instances/instances.js | 8 +- .../public/components/apm/overview/index.js | 14 +- .../public/components/beats/beat/beat.js | 3 + .../components/beats/listing/listing.js | 7 +- .../__snapshots__/overview.test.js.snap | 236 +++++++++--------- .../components/beats/overview/overview.js | 75 +++--- .../components/cluster/overview/apm_panel.js | 16 +- .../cluster/overview/beats_panel.js | 4 +- .../cluster/overview/elasticsearch_panel.js | 38 +-- .../components/cluster/overview/helpers.js | 30 ++- .../cluster/overview/kibana_panel.js | 8 +- .../cluster/overview/logstash_panel.js | 12 +- .../elasticsearch/indices/indices.js | 2 +- .../__snapshots__/cells.test.js.snap | 104 +++++--- .../nodes/__tests__/cells.test.js | 2 + .../components/elasticsearch/nodes/cells.js | 125 +++++++--- .../components/elasticsearch/nodes/nodes.js | 19 +- .../shard_allocation/components/table_head.js | 8 +- .../components/setup_mode/enter_button.scss | 5 +- .../public/directives/beats/beat/index.js | 36 --- .../public/directives/beats/overview/index.js | 30 --- .../public/directives/main/index.html | 53 ++-- .../public/directives/main/index.js | 10 +- .../public/directives/main/index.scss | 3 + .../monitoring/public/services/breadcrumbs.js | 6 +- .../public/views/apm/instance/index.js | 13 +- .../public/views/apm/instances/index.js | 5 +- .../public/views/apm/overview/index.js | 8 +- .../public/views/base_controller.js | 3 + .../public/views/base_eui_table_controller.js | 2 + .../public/views/beats/beat/index.html | 3 +- .../public/views/beats/beat/index.js | 23 +- .../public/views/beats/listing/index.js | 3 + .../public/views/beats/overview/index.html | 2 +- .../public/views/beats/overview/index.js | 17 +- .../public/views/cluster/listing/index.js | 4 + .../public/views/cluster/overview/index.js | 3 + .../public/views/elasticsearch/ccr/index.js | 3 + .../views/elasticsearch/ccr/shard/index.js | 9 + .../public/views/elasticsearch/index/index.js | 6 + .../views/elasticsearch/indices/index.js | 3 + .../views/elasticsearch/ml_jobs/index.js | 3 + .../elasticsearch/node/advanced/index.js | 12 +- .../public/views/elasticsearch/node/index.js | 26 +- .../public/views/elasticsearch/nodes/index.js | 3 + .../elasticsearch/overview/controller.js | 4 + .../public/views/kibana/instance/index.js | 9 + .../public/views/kibana/instances/index.js | 8 +- .../public/views/kibana/overview/index.js | 4 + .../views/logstash/node/advanced/index.js | 9 + .../public/views/logstash/node/index.js | 9 + .../views/logstash/node/pipelines/index.js | 9 + .../public/views/logstash/nodes/index.js | 8 +- .../public/views/logstash/overview/index.js | 4 + .../public/views/logstash/pipeline/index.js | 8 + .../public/views/logstash/pipelines/index.js | 7 +- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - .../apps/monitoring/cluster/overview.js | 10 +- .../apps/monitoring/elasticsearch/nodes.js | 91 +++++-- .../monitoring/elasticsearch_nodes.js | 43 +++- 63 files changed, 792 insertions(+), 462 deletions(-) delete mode 100644 x-pack/plugins/monitoring/public/directives/beats/beat/index.js delete mode 100644 x-pack/plugins/monitoring/public/directives/beats/overview/index.js create mode 100644 x-pack/plugins/monitoring/public/directives/main/index.scss diff --git a/x-pack/plugins/monitoring/public/angular/app_modules.ts b/x-pack/plugins/monitoring/public/angular/app_modules.ts index 499610045d771..4ef905fd35fc4 100644 --- a/x-pack/plugins/monitoring/public/angular/app_modules.ts +++ b/x-pack/plugins/monitoring/public/angular/app_modules.ts @@ -41,10 +41,6 @@ import { licenseProvider } from '../services/license'; // @ts-ignore import { titleProvider } from '../services/title'; // @ts-ignore -import { monitoringBeatsBeatProvider } from '../directives/beats/beat'; -// @ts-ignore -import { monitoringBeatsOverviewProvider } from '../directives/beats/overview'; -// @ts-ignore import { monitoringMlListingProvider } from '../directives/elasticsearch/ml_job_listing'; // @ts-ignore import { monitoringMainProvider } from '../directives/main'; @@ -153,8 +149,6 @@ function createMonitoringAppServices() { function createMonitoringAppDirectives() { angular .module('monitoring/directives', []) - .directive('monitoringBeatsBeat', monitoringBeatsBeatProvider) - .directive('monitoringBeatsOverview', monitoringBeatsOverviewProvider) .directive('monitoringMlListing', monitoringMlListingProvider) .directive('monitoringMain', monitoringMainProvider); } diff --git a/x-pack/plugins/monitoring/public/components/apm/instance/instance.js b/x-pack/plugins/monitoring/public/components/apm/instance/instance.js index 396d2258edd0c..eec24e741ac41 100644 --- a/x-pack/plugins/monitoring/public/components/apm/instance/instance.js +++ b/x-pack/plugins/monitoring/public/components/apm/instance/instance.js @@ -42,9 +42,7 @@ export function ApmServerInstance({ summary, metrics, ...props }) { const charts = seriesToShow.map((data, index) => ( - - - + )); @@ -55,15 +53,15 @@ export function ApmServerInstance({ summary, metrics, ...props }) {

+ + + + - - - - {charts} diff --git a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js index 6dcfa6dd043aa..e05ba1878caed 100644 --- a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js +++ b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js @@ -156,11 +156,11 @@ export function ApmServerInstances({ apms, setupMode }) { /> + + + + - - - - {setupModeCallout} ( - - - + )); @@ -51,15 +49,15 @@ export function ApmOverview({ stats, metrics, ...props }) {

+ + + + - - - - {charts} diff --git a/x-pack/plugins/monitoring/public/components/beats/beat/beat.js b/x-pack/plugins/monitoring/public/components/beats/beat/beat.js index 3fe211c0f2edc..f489271659bfe 100644 --- a/x-pack/plugins/monitoring/public/components/beats/beat/beat.js +++ b/x-pack/plugins/monitoring/public/components/beats/beat/beat.js @@ -135,6 +135,9 @@ export function Beat({ summary, metrics, ...props }) { + + + diff --git a/x-pack/plugins/monitoring/public/components/beats/listing/listing.js b/x-pack/plugins/monitoring/public/components/beats/listing/listing.js index be8595e8e6bbe..60a35e00a4c63 100644 --- a/x-pack/plugins/monitoring/public/components/beats/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/beats/listing/listing.js @@ -13,6 +13,7 @@ import { EuiSpacer, EuiLink, EuiScreenReaderOnly, + EuiPanel, } from '@elastic/eui'; import { Stats } from '../../beats'; import { formatMetric } from '../../../lib/format_number'; @@ -153,9 +154,11 @@ export class Listing extends PureComponent { /> - + - + + + {setupModeCallOut} - + + + + - - -

- -

-
- - -
+ +

+ +

+
+ +
- - -

- -

-
- - -
+ +

+ +

+
+ +
- - -

- -

-
- - -
+ +

+ +

+
+ +
- +
+ + @@ -212,18 +213,25 @@ exports[`Overview that overview page shows a message if there is no beats data 1 /> - + + + + - + + + diff --git a/x-pack/plugins/monitoring/public/components/beats/overview/overview.js b/x-pack/plugins/monitoring/public/components/beats/overview/overview.js index 83f92ea1b481c..897f017f44f41 100644 --- a/x-pack/plugins/monitoring/public/components/beats/overview/overview.js +++ b/x-pack/plugins/monitoring/public/components/beats/overview/overview.js @@ -30,46 +30,40 @@ function renderLatestActive(latestActive, latestTypes, latestVersions) { return ( - - -

- -

-
- - -
+ +

+ +

+
+ +
- - -

- -

-
- - -
+ +

+ +

+
+ +
- - -

- -

-
- - -
+ +

+ +

+
+ +
); @@ -118,10 +112,13 @@ export function BeatsOverview({ /> - + - {renderLatestActive(latestActive, latestTypes, latestVersions)} - + + + {renderLatestActive(latestActive, latestTypes, latestVersions)} + + {charts}
diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js index ccbf0b0ec711d..4bf07710393ea 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js @@ -55,7 +55,7 @@ export function ApmPanel(props) { {...props} url="apm" title={i18n.translate('xpack.monitoring.cluster.overview.apmPanel.apmTitle', { - defaultMessage: 'APM', + defaultMessage: 'APM server', })} > @@ -70,21 +70,21 @@ export function ApmPanel(props) { aria-label={i18n.translate( 'xpack.monitoring.cluster.overview.apmPanel.overviewLinkAriaLabel', { - defaultMessage: 'APM Overview', + defaultMessage: 'APM server overview', } )} data-test-subj="apmOverview" >
- + {formatMetric(props.totalEvents, '0.[0]a')} - + {apmsTotal} }} />
@@ -144,7 +144,7 @@ export function ApmPanel(props) { - + - + {formatMetric(props.totalEvents, '0.[0]a')} - + {props.logs.types.map((log, index) => ( - + - + @@ -276,7 +277,7 @@ export function ElasticsearchPanel(props) { - + - + - + - + {showMlJobs()} - + - + - + - + - + {formatNumber(get(indices, 'docs.count'), 'int_commas')} - + - + - + - + diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js index 6fa533302db48..7df0a3ca7138e 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js @@ -111,7 +111,7 @@ export function KibanaPanel(props) { data-test-subj="kibana_overview" data-overview-status={props.status} > - + {props.requests_total} - + - + {formatNumber(props.concurrent_connections, 'int_commas')} - + - + {formatNumber(props.events_in_total, '0.[0]a')} - + - + {props.max_uptime ? formatNumber(props.max_uptime, 'time_since') : 0} - + - + {queueTypes[LOGSTASH.QUEUE_TYPES.MEMORY] || 0} - + } checked={showSystemIndices} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap index c7081dc439085..b0b5ceb46d16c 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap @@ -10,30 +10,46 @@ exports[`Node Listing Metric Cell should format N/A as the metric for an offline exports[`Node Listing Metric Cell should format a non-percentage metric 1`] = `
+
- - 206.3 GB  - - -
- 206.5 GB max -
- 206.3 GB min +
+
+
+
+
+
+
+
+
+ 206.3 GB +
+
@@ -41,30 +57,46 @@ exports[`Node Listing Metric Cell should format a non-percentage metric 1`] = ` exports[`Node Listing Metric Cell should format a percentage metric 1`] = `
+
- - 0%  - - -
- 2% max -
- 0% min +
+
+
+
+
+
+
+
+
+ 0% +
+
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js index 0c4b4b2b3c3f4..f0b131b65433c 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js @@ -27,6 +27,7 @@ describe('Node Listing Metric Cell', () => { }, summary: { minVal: 0, maxVal: 2, lastVal: 0, slope: -1 }, }, + 'data-test-subj': 'testCell', }; expect(renderWithIntl()).toMatchSnapshot(); }); @@ -54,6 +55,7 @@ describe('Node Listing Metric Cell', () => { slope: -1, }, }, + 'data-test-subj': 'testCell2', }; expect(renderWithIntl()).toMatchSnapshot(); }); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js index 4c3b642213d99..9956dd4da7d8a 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js @@ -4,19 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useState } from 'react'; import { get } from 'lodash'; import { formatMetric } from '../../../lib/format_number'; -import { EuiText, EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiText, + EuiPopover, + EuiIcon, + EuiDescriptionList, + EuiSpacer, + EuiKeyboardAccessible, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +const TRENDING_DOWN = i18n.translate('xpack.monitoring.elasticsearch.node.cells.trendingDownText', { + defaultMessage: 'down', +}); +const TRENDING_UP = i18n.translate('xpack.monitoring.elasticsearch.node.cells.trendingUpText', { + defaultMessage: 'up', +}); + function OfflineCell() { return
N/A
; } -const getSlopeArrow = (slope) => { +const getDirection = (slope) => { + if (slope || slope === 0) { + return slope > 0 ? TRENDING_UP : TRENDING_DOWN; + } + return null; +}; + +const getIcon = (slope) => { if (slope || slope === 0) { - return slope > 0 ? 'up' : 'down'; + return slope > 0 ? 'arrowUp' : 'arrowDown'; } return null; }; @@ -28,40 +51,82 @@ const metricVal = (metric, format, isPercent, units) => { return formatMetric(metric, format, units); }; -const noWrapStyle = { overflowX: 'hidden', whiteSpace: 'nowrap' }; - function MetricCell({ isOnline, metric = {}, isPercent, ...props }) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const onButtonClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen); + const closePopover = () => setIsPopoverOpen(false); + if (isOnline) { const { lastVal, maxVal, minVal, slope } = get(metric, 'summary', {}); const format = get(metric, 'metric.format'); const units = get(metric, 'metric.units'); + const tooltipItems = [ + { + title: i18n.translate('xpack.monitoring.elasticsearch.node.cells.tooltip.trending', { + defaultMessage: 'Trending', + }), + description: getDirection(slope), + }, + { + title: i18n.translate('xpack.monitoring.elasticsearch.node.cells.tooltip.max', { + defaultMessage: 'Max value', + }), + description: metricVal(maxVal, format, isPercent, units), + }, + { + title: i18n.translate('xpack.monitoring.elasticsearch.node.cells.tooltip.min', { + defaultMessage: 'Min value', + }), + description: metricVal(minVal, format, isPercent, units), + }, + ]; + + const button = ( + + + + ); + return ( - + + - - - {metricVal(lastVal, format, isPercent)} -   - - - - - {i18n.translate('xpack.monitoring.elasticsearch.nodes.cells.maxText', { - defaultMessage: '{metric} max', - values: { - metric: metricVal(maxVal, format, isPercent, units), - }, - })} - - - {i18n.translate('xpack.monitoring.elasticsearch.nodes.cells.minText', { - defaultMessage: '{metric} min', - values: { - metric: metricVal(minVal, format, isPercent, units), - }, - })} - + + + +
+ + + + {i18n.translate('xpack.monitoring.elasticsearch.node.cells.tooltip.preface', { + defaultMessage: 'Applies to current time period', + })} + +
+
+
+ + {metricVal(lastVal, format, isPercent)} + +
); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js index 43512f8e528f6..f088f7c0d348a 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js @@ -73,7 +73,6 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler name: i18n.translate('xpack.monitoring.elasticsearch.nodes.nameColumnTitle', { defaultMessage: 'Name', }), - width: '20%', field: 'name', sortable: true, render: (value, node) => { @@ -131,7 +130,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler defaultMessage: 'Alerts', }), field: 'alerts', - width: '175px', + // width: '175px', sortable: true, render: (_field, node) => { return ( @@ -148,6 +147,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler name: i18n.translate('xpack.monitoring.elasticsearch.nodes.statusColumnTitle', { defaultMessage: 'Status', }), + dataType: 'boolean', field: 'isOnline', sortable: true, render: (value) => { @@ -181,22 +181,18 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler name: i18n.translate('xpack.monitoring.elasticsearch.nodes.shardsColumnTitle', { defaultMessage: 'Shards', }), + dataType: 'number', field: 'shardCount', sortable: true, render: (value, node) => { - return node.isOnline ? ( -
- {value} -
- ) : ( - - ); + return node.isOnline ? {value} : ; }, }); if (showCgroupMetricsElasticsearch) { cols.push({ name: cpuUsageColumnTitle, + dataType: 'number', field: 'node_cgroup_quota', sortable: getSortHandler('node_cgroup_quota'), render: (value, node) => ( @@ -213,6 +209,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler name: i18n.translate('xpack.monitoring.elasticsearch.nodes.cpuThrottlingColumnTitle', { defaultMessage: 'CPU Throttling', }), + dataType: 'number', field: 'node_cgroup_throttled', sortable: getSortHandler('node_cgroup_throttled'), render: (value, node) => ( @@ -227,6 +224,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler } else { cols.push({ name: cpuUsageColumnTitle, + dataType: 'number', field: 'node_cpu_utilization', sortable: getSortHandler('node_cpu_utilization'), render: (value, node) => { @@ -245,6 +243,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler name: i18n.translate('xpack.monitoring.elasticsearch.nodes.loadAverageColumnTitle', { defaultMessage: 'Load Average', }), + dataType: 'number', field: 'node_load_average', sortable: getSortHandler('node_load_average'), render: (value, node) => ( @@ -265,6 +264,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler javaVirtualMachine: 'JVM', }, }), + dataType: 'number', field: 'node_jvm_mem_percent', sortable: getSortHandler('node_jvm_mem_percent'), render: (value, node) => ( @@ -281,6 +281,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler name: i18n.translate('xpack.monitoring.elasticsearch.nodes.diskFreeSpaceColumnTitle', { defaultMessage: 'Disk Free Space', }), + dataType: 'number', field: 'node_free_space', sortable: getSortHandler('node_free_space'), render: (value, node) => ( diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js index fd5f28ea02039..3c875667fe04c 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js @@ -5,6 +5,7 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -37,7 +38,12 @@ class IndexLabel extends React.Component { $el && $el[0] && unmountComponentAtNode($el[0])); - - scope.$watch('data', (data = {}) => { - render( - , - $el[0] - ); - }); - }, - }; -} diff --git a/x-pack/plugins/monitoring/public/directives/beats/overview/index.js b/x-pack/plugins/monitoring/public/directives/beats/overview/index.js deleted file mode 100644 index 4faf69e13d02c..0000000000000 --- a/x-pack/plugins/monitoring/public/directives/beats/overview/index.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { BeatsOverview } from '../../../components/beats/overview'; - -export function monitoringBeatsOverviewProvider() { - return { - restrict: 'E', - scope: { - data: '=', - onBrush: '<', - zoomInfo: '<', - }, - link(scope, $el) { - scope.$on('$destroy', () => $el && $el[0] && unmountComponentAtNode($el[0])); - - scope.$watch('data', (data = {}) => { - render( - , - $el[0] - ); - }); - }, - }; -} diff --git a/x-pack/plugins/monitoring/public/directives/main/index.html b/x-pack/plugins/monitoring/public/directives/main/index.html index fabd207d72b1f..fb24d9e678d56 100644 --- a/x-pack/plugins/monitoring/public/directives/main/index.html +++ b/x-pack/plugins/monitoring/public/directives/main/index.html @@ -1,19 +1,32 @@
-
- - +
+
+
+
+
+
+

{{pageTitle || monitoringMain.instance}}

+
+
+
+
+
+ + +
+
From 14921a037e7e9a2cc607873ae583b4c7303a4e56 Mon Sep 17 00:00:00 2001 From: Phillip Burch Date: Wed, 23 Sep 2020 13:27:28 -0500 Subject: [PATCH 60/92] [Metrics UI] Anomaly Detection setup flow for Metrics (#76787) * adds metrics ml integration * Add ability to create ml jobs from inventory * Fix i18n stuff * Fix typecheck * renames jobs, updates datafeeds * adds allow_no_indices: true for datafeeds * Revert "[Metrics UI] Replace Snapshot API with Metrics API (#76253)" This reverts commit 0ca647286a5ccabfb76203b8cbcb1d13b05f105d. * Add ability to fetch anomalies * Fix typecheck * Fix typecheck * Fix i18n * Fix lint, use the right partition field * Delete log files * Fix merge * Fix merge issues * Update name of jobs * Remove CPU job * [Metrics UI] Replace Snapshot API with Metrics API (#76253) - Remove server/lib/snapshot - Replace backend for /api/infra/snapshot with data from Metrics API - Fixing tests with updates to the snapshot node Co-authored-by: Elastic Machine * Add links back to ML for anomalies and manage jobs * Fix typecheck * Remove unecessary validation Co-authored-by: Michael Hirsch Co-authored-by: Elastic Machine Co-authored-by: Chris Cowan --- .../infra/common/http_api/infra_ml/index.ts | 7 + .../http_api/infra_ml/results/common.ts | 59 ++++ .../common/http_api/infra_ml/results/index.ts | 9 + .../results/metrics_hosts_anomalies.ts | 79 +++++ .../infra_ml/results/metrics_k8s_anomalies.ts | 79 +++++ .../infra/common/infra_ml/anomaly_results.ts | 57 +++ x-pack/plugins/infra/common/infra_ml/index.ts | 11 + .../plugins/infra/common/infra_ml/infra_ml.ts | 52 +++ .../infra/common/infra_ml/job_parameters.ts | 93 +++++ .../infra/common/infra_ml/metrics_hosts_ml.ts | 21 ++ .../infra/common/infra_ml/metrics_k8s_ml.ts | 21 ++ .../public/containers/ml/api/ml_api_types.ts | 28 ++ .../public/containers/ml/api/ml_cleanup.ts | 95 +++++ .../ml/api/ml_get_jobs_summary_api.ts | 93 +++++ .../public/containers/ml/api/ml_get_module.ts | 41 +++ .../containers/ml/api/ml_setup_module_api.ts | 115 ++++++ .../containers/ml/infra_ml_capabilities.tsx | 97 +++++ .../public/containers/ml/infra_ml_cleanup.tsx | 55 +++ .../public/containers/ml/infra_ml_module.tsx | 147 ++++++++ .../ml/infra_ml_module_configuration.ts | 52 +++ .../ml/infra_ml_module_definition.tsx | 76 ++++ .../containers/ml/infra_ml_module_status.tsx | 268 ++++++++++++++ .../containers/ml/infra_ml_module_types.ts | 93 +++++ .../containers/ml/infra_ml_setup_state.ts | 289 +++++++++++++++ .../ml/modules/metrics_hosts/module.tsx | 80 +++++ .../metrics_hosts/module_descriptor.ts | 126 +++++++ .../ml/modules/metrics_k8s/module.tsx | 80 +++++ .../modules/metrics_k8s/module_descriptor.ts | 129 +++++++ .../infra/public/pages/metrics/index.tsx | 210 +++++------ .../anomoly_detection_flyout.tsx | 92 +++++ .../ml/anomaly_detection/flyout_home.tsx | 333 ++++++++++++++++++ .../ml/anomaly_detection/job_setup_screen.tsx | 277 +++++++++++++++ .../subscription_splash_content.tsx | 172 +++++++++ .../hooks/use_metrics_hosts_anomalies.ts | 318 +++++++++++++++++ .../hooks/use_metrics_k8s_anomalies.ts | 322 +++++++++++++++++ x-pack/plugins/infra/server/infra_server.ts | 4 + .../infra/server/lib/infra_ml/common.ts | 89 +++++ .../infra/server/lib/infra_ml/errors.ts | 49 +++ .../infra/server/lib/infra_ml/index.ts | 9 + .../lib/infra_ml/metrics_hosts_anomalies.ts | 289 +++++++++++++++ .../lib/infra_ml/metrics_k8s_anomalies.ts | 272 ++++++++++++++ .../server/lib/infra_ml/queries/common.ts | 68 ++++ .../server/lib/infra_ml/queries/index.ts | 7 + .../infra_ml/queries/log_entry_data_sets.ts | 84 +++++ .../queries/metrics_hosts_anomalies.ts | 131 +++++++ .../infra_ml/queries/metrics_k8s_anomalies.ts | 128 +++++++ .../server/lib/infra_ml/queries/ml_jobs.ts | 24 ++ .../infra/server/routes/infra_ml/index.ts | 7 + .../server/routes/infra_ml/results/index.ts | 8 + .../results/metrics_hosts_anomalies.ts | 125 +++++++ .../infra_ml/results/metrics_k8s_anomalies.ts | 122 +++++++ 51 files changed, 5392 insertions(+), 100 deletions(-) create mode 100644 x-pack/plugins/infra/common/http_api/infra_ml/index.ts create mode 100644 x-pack/plugins/infra/common/http_api/infra_ml/results/common.ts create mode 100644 x-pack/plugins/infra/common/http_api/infra_ml/results/index.ts create mode 100644 x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts create mode 100644 x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts create mode 100644 x-pack/plugins/infra/common/infra_ml/anomaly_results.ts create mode 100644 x-pack/plugins/infra/common/infra_ml/index.ts create mode 100644 x-pack/plugins/infra/common/infra_ml/infra_ml.ts create mode 100644 x-pack/plugins/infra/common/infra_ml/job_parameters.ts create mode 100644 x-pack/plugins/infra/common/infra_ml/metrics_hosts_ml.ts create mode 100644 x-pack/plugins/infra/common/infra_ml/metrics_k8s_ml.ts create mode 100644 x-pack/plugins/infra/public/containers/ml/api/ml_api_types.ts create mode 100644 x-pack/plugins/infra/public/containers/ml/api/ml_cleanup.ts create mode 100644 x-pack/plugins/infra/public/containers/ml/api/ml_get_jobs_summary_api.ts create mode 100644 x-pack/plugins/infra/public/containers/ml/api/ml_get_module.ts create mode 100644 x-pack/plugins/infra/public/containers/ml/api/ml_setup_module_api.ts create mode 100644 x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx create mode 100644 x-pack/plugins/infra/public/containers/ml/infra_ml_cleanup.tsx create mode 100644 x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx create mode 100644 x-pack/plugins/infra/public/containers/ml/infra_ml_module_configuration.ts create mode 100644 x-pack/plugins/infra/public/containers/ml/infra_ml_module_definition.tsx create mode 100644 x-pack/plugins/infra/public/containers/ml/infra_ml_module_status.tsx create mode 100644 x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts create mode 100644 x-pack/plugins/infra/public/containers/ml/infra_ml_setup_state.ts create mode 100644 x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module.tsx create mode 100644 x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts create mode 100644 x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module.tsx create mode 100644 x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/flyout_home.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts create mode 100644 x-pack/plugins/infra/server/lib/infra_ml/common.ts create mode 100644 x-pack/plugins/infra/server/lib/infra_ml/errors.ts create mode 100644 x-pack/plugins/infra/server/lib/infra_ml/index.ts create mode 100644 x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts create mode 100644 x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts create mode 100644 x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts create mode 100644 x-pack/plugins/infra/server/lib/infra_ml/queries/index.ts create mode 100644 x-pack/plugins/infra/server/lib/infra_ml/queries/log_entry_data_sets.ts create mode 100644 x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts create mode 100644 x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts create mode 100644 x-pack/plugins/infra/server/lib/infra_ml/queries/ml_jobs.ts create mode 100644 x-pack/plugins/infra/server/routes/infra_ml/index.ts create mode 100644 x-pack/plugins/infra/server/routes/infra_ml/results/index.ts create mode 100644 x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts create mode 100644 x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts diff --git a/x-pack/plugins/infra/common/http_api/infra_ml/index.ts b/x-pack/plugins/infra/common/http_api/infra_ml/index.ts new file mode 100644 index 0000000000000..38684cb22e237 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/infra_ml/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './results'; diff --git a/x-pack/plugins/infra/common/http_api/infra_ml/results/common.ts b/x-pack/plugins/infra/common/http_api/infra_ml/results/common.ts new file mode 100644 index 0000000000000..0474fbd1cfc2f --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/infra_ml/results/common.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +// [Sort field value, tiebreaker value] +export const paginationCursorRT = rt.tuple([ + rt.union([rt.string, rt.number]), + rt.union([rt.string, rt.number]), +]); + +export type PaginationCursor = rt.TypeOf; + +export const anomalyTypeRT = rt.keyof({ + metrics_hosts: null, + metrics_k8s: null, +}); + +export type AnomalyType = rt.TypeOf; + +const sortOptionsRT = rt.keyof({ + anomalyScore: null, + dataset: null, + startTime: null, +}); + +const sortDirectionsRT = rt.keyof({ + asc: null, + desc: null, +}); + +const paginationPreviousPageCursorRT = rt.type({ + searchBefore: paginationCursorRT, +}); + +const paginationNextPageCursorRT = rt.type({ + searchAfter: paginationCursorRT, +}); + +export const paginationRT = rt.intersection([ + rt.type({ + pageSize: rt.number, + }), + rt.partial({ + cursor: rt.union([paginationPreviousPageCursorRT, paginationNextPageCursorRT]), + }), +]); + +export type Pagination = rt.TypeOf; + +export const sortRT = rt.type({ + field: sortOptionsRT, + direction: sortDirectionsRT, +}); + +export type Sort = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/http_api/infra_ml/results/index.ts b/x-pack/plugins/infra/common/http_api/infra_ml/results/index.ts new file mode 100644 index 0000000000000..efd597a043e07 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/infra_ml/results/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './metrics_hosts_anomalies'; +export * from './metrics_k8s_anomalies'; +export * from './common'; diff --git a/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts new file mode 100644 index 0000000000000..9fdac09fec20e --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +import { timeRangeRT, routeTimingMetadataRT } from '../../shared'; +import { anomalyTypeRT, paginationCursorRT, sortRT, paginationRT } from './common'; + +export const INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH = + '/api/infra/infra_ml/results/metrics_hosts_anomalies'; + +const metricsHostAnomalyCommonFieldsRT = rt.type({ + id: rt.string, + anomalyScore: rt.number, + typical: rt.number, + actual: rt.number, + type: anomalyTypeRT, + duration: rt.number, + startTime: rt.number, + jobId: rt.string, +}); +const metricsHostsAnomalyRT = metricsHostAnomalyCommonFieldsRT; + +export type MetricsHostsAnomaly = rt.TypeOf; + +export const getMetricsHostsAnomaliesSuccessReponsePayloadRT = rt.intersection([ + rt.type({ + data: rt.intersection([ + rt.type({ + anomalies: rt.array(metricsHostsAnomalyRT), + // Signifies there are more entries backwards or forwards. If this was a request + // for a previous page, there are more previous pages, if this was a request for a next page, + // there are more next pages. + hasMoreEntries: rt.boolean, + }), + rt.partial({ + paginationCursors: rt.type({ + // The cursor to use to fetch the previous page + previousPageCursor: paginationCursorRT, + // The cursor to use to fetch the next page + nextPageCursor: paginationCursorRT, + }), + }), + ]), + }), + rt.partial({ + timing: routeTimingMetadataRT, + }), +]); + +export type GetMetricsHostsAnomaliesSuccessResponsePayload = rt.TypeOf< + typeof getMetricsHostsAnomaliesSuccessReponsePayloadRT +>; + +export const getMetricsHostsAnomaliesRequestPayloadRT = rt.type({ + data: rt.intersection([ + rt.type({ + // the ID of the source configuration + sourceId: rt.string, + // the time range to fetch the log entry anomalies from + timeRange: timeRangeRT, + }), + rt.partial({ + // Pagination properties + pagination: paginationRT, + // Sort properties + sort: sortRT, + // // Dataset filters + // datasets: rt.array(rt.string), + }), + ]), +}); + +export type GetMetricsHostsAnomaliesRequestPayload = rt.TypeOf< + typeof getMetricsHostsAnomaliesRequestPayloadRT +>; diff --git a/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts new file mode 100644 index 0000000000000..ab1f245a74c0c --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +import { timeRangeRT, routeTimingMetadataRT } from '../../shared'; +import { paginationCursorRT, anomalyTypeRT, sortRT, paginationRT } from './common'; + +export const INFA_ML_GET_METRICS_K8S_ANOMALIES_PATH = + '/api/infra/infra_ml/results/metrics_k8s_anomalies'; + +const metricsK8sAnomalyCommonFieldsRT = rt.type({ + id: rt.string, + anomalyScore: rt.number, + typical: rt.number, + actual: rt.number, + type: anomalyTypeRT, + duration: rt.number, + startTime: rt.number, + jobId: rt.string, +}); +const metricsK8sAnomalyRT = metricsK8sAnomalyCommonFieldsRT; + +export type MetricsK8sAnomaly = rt.TypeOf; + +export const getMetricsK8sAnomaliesSuccessReponsePayloadRT = rt.intersection([ + rt.type({ + data: rt.intersection([ + rt.type({ + anomalies: rt.array(metricsK8sAnomalyRT), + // Signifies there are more entries backwards or forwards. If this was a request + // for a previous page, there are more previous pages, if this was a request for a next page, + // there are more next pages. + hasMoreEntries: rt.boolean, + }), + rt.partial({ + paginationCursors: rt.type({ + // The cursor to use to fetch the previous page + previousPageCursor: paginationCursorRT, + // The cursor to use to fetch the next page + nextPageCursor: paginationCursorRT, + }), + }), + ]), + }), + rt.partial({ + timing: routeTimingMetadataRT, + }), +]); + +export type GetMetricsK8sAnomaliesSuccessResponsePayload = rt.TypeOf< + typeof getMetricsK8sAnomaliesSuccessReponsePayloadRT +>; + +export const getMetricsK8sAnomaliesRequestPayloadRT = rt.type({ + data: rt.intersection([ + rt.type({ + // the ID of the source configuration + sourceId: rt.string, + // the time range to fetch the log entry anomalies from + timeRange: timeRangeRT, + }), + rt.partial({ + // Pagination properties + pagination: paginationRT, + // Sort properties + sort: sortRT, + // Dataset filters + datasets: rt.array(rt.string), + }), + ]), +}); + +export type GetMetricsK8sAnomaliesRequestPayload = rt.TypeOf< + typeof getMetricsK8sAnomaliesRequestPayloadRT +>; diff --git a/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts b/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts new file mode 100644 index 0000000000000..f4497dbba5056 --- /dev/null +++ b/x-pack/plugins/infra/common/infra_ml/anomaly_results.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +export const ML_SEVERITY_SCORES = { + warning: 3, + minor: 25, + major: 50, + critical: 75, +}; + +export type MLSeverityScoreCategories = keyof typeof ML_SEVERITY_SCORES; + +export const ML_SEVERITY_COLORS = { + critical: 'rgb(228, 72, 72)', + major: 'rgb(229, 113, 0)', + minor: 'rgb(255, 221, 0)', + warning: 'rgb(125, 180, 226)', +}; + +export const getSeverityCategoryForScore = ( + score: number +): MLSeverityScoreCategories | undefined => { + if (score >= ML_SEVERITY_SCORES.critical) { + return 'critical'; + } else if (score >= ML_SEVERITY_SCORES.major) { + return 'major'; + } else if (score >= ML_SEVERITY_SCORES.minor) { + return 'minor'; + } else if (score >= ML_SEVERITY_SCORES.warning) { + return 'warning'; + } else { + // Category is too low to include + return undefined; + } +}; + +export const formatAnomalyScore = (score: number) => { + return Math.round(score); +}; + +export const formatOneDecimalPlace = (number: number) => { + return Math.round(number * 10) / 10; +}; + +export const getFriendlyNameForPartitionId = (partitionId: string) => { + return partitionId !== '' ? partitionId : 'unknown'; +}; + +export const compareDatasetsByMaximumAnomalyScore = < + Dataset extends { maximumAnomalyScore: number } +>( + firstDataset: Dataset, + secondDataset: Dataset +) => firstDataset.maximumAnomalyScore - secondDataset.maximumAnomalyScore; diff --git a/x-pack/plugins/infra/common/infra_ml/index.ts b/x-pack/plugins/infra/common/infra_ml/index.ts new file mode 100644 index 0000000000000..88fbbd4f25045 --- /dev/null +++ b/x-pack/plugins/infra/common/infra_ml/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './infra_ml'; +export * from './anomaly_results'; +export * from './job_parameters'; +export * from './metrics_hosts_ml'; +export * from './metrics_k8s_ml'; diff --git a/x-pack/plugins/infra/common/infra_ml/infra_ml.ts b/x-pack/plugins/infra/common/infra_ml/infra_ml.ts new file mode 100644 index 0000000000000..680a2a0fef114 --- /dev/null +++ b/x-pack/plugins/infra/common/infra_ml/infra_ml.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; + * you may not use this file except in compliance with the Elastic License. + */ + +// combines and abstracts job and datafeed status +export type JobStatus = + | 'unknown' + | 'missing' + | 'initializing' + | 'stopped' + | 'started' + | 'finished' + | 'failed'; + +export type SetupStatus = + | { type: 'initializing' } // acquiring job statuses to determine setup status + | { type: 'unknown' } // job status could not be acquired (failed request etc) + | { type: 'required' } // setup required + | { type: 'pending' } // In the process of setting up the module for the first time or retrying, waiting for response + | { type: 'succeeded' } // setup succeeded, notifying user + | { + type: 'failed'; + reasons: string[]; + } // setup failed, notifying user + | { + type: 'skipped'; + newlyCreated?: boolean; + }; // setup is not necessary + +/** + * Maps a job status to the possibility that results have already been produced + * before this state was reached. + */ +export const isJobStatusWithResults = (jobStatus: JobStatus) => + ['started', 'finished', 'stopped', 'failed'].includes(jobStatus); + +export const isHealthyJobStatus = (jobStatus: JobStatus) => + ['started', 'finished'].includes(jobStatus); + +/** + * Maps a setup status to the possibility that results have already been + * produced before this state was reached. + */ +export const isSetupStatusWithResults = (setupStatus: SetupStatus) => + setupStatus.type === 'skipped'; + +const KIBANA_SAMPLE_DATA_INDICES = ['kibana_sample_data_logs*']; + +export const isExampleDataIndex = (indexName: string) => + KIBANA_SAMPLE_DATA_INDICES.includes(indexName); diff --git a/x-pack/plugins/infra/common/infra_ml/job_parameters.ts b/x-pack/plugins/infra/common/infra_ml/job_parameters.ts new file mode 100644 index 0000000000000..8cd1c9ea6e2ba --- /dev/null +++ b/x-pack/plugins/infra/common/infra_ml/job_parameters.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const bucketSpan = 900000; + +export const categoriesMessageField = 'message'; + +export const partitionField = 'event.dataset'; + +export const getJobIdPrefix = (spaceId: string, sourceId: string) => + `kibana-metrics-ui-${spaceId}-${sourceId}-`; + +export const getJobId = (spaceId: string, sourceId: string, jobType: string) => + `${getJobIdPrefix(spaceId, sourceId)}${jobType}`; + +export const getDatafeedId = (spaceId: string, sourceId: string, jobType: string) => + `datafeed-${getJobId(spaceId, sourceId, jobType)}`; + +export const datasetFilterRT = rt.union([ + rt.strict({ + type: rt.literal('includeAll'), + }), + rt.strict({ + type: rt.literal('includeSome'), + datasets: rt.array(rt.string), + }), +]); + +export type DatasetFilter = rt.TypeOf; + +export const jobSourceConfigurationRT = rt.partial({ + indexPattern: rt.string, + timestampField: rt.string, + bucketSpan: rt.number, + datasetFilter: datasetFilterRT, +}); + +export type JobSourceConfiguration = rt.TypeOf; + +export const jobCustomSettingsRT = rt.partial({ + job_revision: rt.number, + metrics_source_config: jobSourceConfigurationRT, +}); + +export type JobCustomSettings = rt.TypeOf; + +export const combineDatasetFilters = ( + firstFilter: DatasetFilter, + secondFilter: DatasetFilter +): DatasetFilter => { + if (firstFilter.type === 'includeAll' && secondFilter.type === 'includeAll') { + return { + type: 'includeAll', + }; + } + + const includedDatasets = new Set([ + ...(firstFilter.type === 'includeSome' ? firstFilter.datasets : []), + ...(secondFilter.type === 'includeSome' ? secondFilter.datasets : []), + ]); + + return { + type: 'includeSome', + datasets: [...includedDatasets], + }; +}; + +export const filterDatasetFilter = ( + datasetFilter: DatasetFilter, + predicate: (dataset: string) => boolean +): DatasetFilter => { + if (datasetFilter.type === 'includeAll') { + return datasetFilter; + } else { + const newDatasets = datasetFilter.datasets.filter(predicate); + + if (newDatasets.length > 0) { + return { + type: 'includeSome', + datasets: newDatasets, + }; + } else { + return { + type: 'includeAll', + }; + } + } +}; diff --git a/x-pack/plugins/infra/common/infra_ml/metrics_hosts_ml.ts b/x-pack/plugins/infra/common/infra_ml/metrics_hosts_ml.ts new file mode 100644 index 0000000000000..d09b3be78204f --- /dev/null +++ b/x-pack/plugins/infra/common/infra_ml/metrics_hosts_ml.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const metricsHostsJobTypeRT = rt.keyof({ + hosts_memory_usage: null, + hosts_network_in: null, + hosts_network_out: null, +}); + +export type MetricsHostsJobType = rt.TypeOf; + +export const metricsHostsJobTypes: MetricsHostsJobType[] = [ + 'hosts_memory_usage', + 'hosts_network_in', + 'hosts_network_out', +]; diff --git a/x-pack/plugins/infra/common/infra_ml/metrics_k8s_ml.ts b/x-pack/plugins/infra/common/infra_ml/metrics_k8s_ml.ts new file mode 100644 index 0000000000000..3c27dbb61a14a --- /dev/null +++ b/x-pack/plugins/infra/common/infra_ml/metrics_k8s_ml.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const metricK8sJobTypeRT = rt.keyof({ + k8s_memory_usage: null, + k8s_network_in: null, + k8s_network_out: null, +}); + +export type MetricK8sJobType = rt.TypeOf; + +export const metricsK8SJobTypes: MetricK8sJobType[] = [ + 'k8s_memory_usage', + 'k8s_network_in', + 'k8s_network_out', +]; diff --git a/x-pack/plugins/infra/public/containers/ml/api/ml_api_types.ts b/x-pack/plugins/infra/public/containers/ml/api/ml_api_types.ts new file mode 100644 index 0000000000000..ee70edc31d49b --- /dev/null +++ b/x-pack/plugins/infra/public/containers/ml/api/ml_api_types.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const getMlCapabilitiesResponsePayloadRT = rt.type({ + capabilities: rt.type({ + canGetJobs: rt.boolean, + canCreateJob: rt.boolean, + canDeleteJob: rt.boolean, + canOpenJob: rt.boolean, + canCloseJob: rt.boolean, + canForecastJob: rt.boolean, + canGetDatafeeds: rt.boolean, + canStartStopDatafeed: rt.boolean, + canUpdateJob: rt.boolean, + canUpdateDatafeed: rt.boolean, + canPreviewDatafeed: rt.boolean, + }), + isPlatinumOrTrialLicense: rt.boolean, + mlFeatureEnabledInSpace: rt.boolean, + upgradeInProgress: rt.boolean, +}); + +export type GetMlCapabilitiesResponsePayload = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/containers/ml/api/ml_cleanup.ts b/x-pack/plugins/infra/public/containers/ml/api/ml_cleanup.ts new file mode 100644 index 0000000000000..23fa338e74f14 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/ml/api/ml_cleanup.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { npStart } from '../../../legacy_singletons'; + +import { getDatafeedId, getJobId } from '../../../../common/infra_ml'; +import { throwErrors, createPlainError } from '../../../../common/runtime_types'; + +export const callDeleteJobs = async ( + spaceId: string, + sourceId: string, + jobTypes: JobType[] +) => { + // NOTE: Deleting the jobs via this API will delete the datafeeds at the same time + const deleteJobsResponse = await npStart.http.fetch('/api/ml/jobs/delete_jobs', { + method: 'POST', + body: JSON.stringify( + deleteJobsRequestPayloadRT.encode({ + jobIds: jobTypes.map((jobType) => getJobId(spaceId, sourceId, jobType)), + }) + ), + }); + + return pipe( + deleteJobsResponsePayloadRT.decode(deleteJobsResponse), + fold(throwErrors(createPlainError), identity) + ); +}; + +export const callGetJobDeletionTasks = async () => { + const jobDeletionTasksResponse = await npStart.http.fetch('/api/ml/jobs/deleting_jobs_tasks'); + + return pipe( + getJobDeletionTasksResponsePayloadRT.decode(jobDeletionTasksResponse), + fold(throwErrors(createPlainError), identity) + ); +}; + +export const callStopDatafeeds = async ( + spaceId: string, + sourceId: string, + jobTypes: JobType[] +) => { + // Stop datafeed due to https://github.com/elastic/kibana/issues/44652 + const stopDatafeedResponse = await npStart.http.fetch('/api/ml/jobs/stop_datafeeds', { + method: 'POST', + body: JSON.stringify( + stopDatafeedsRequestPayloadRT.encode({ + datafeedIds: jobTypes.map((jobType) => getDatafeedId(spaceId, sourceId, jobType)), + }) + ), + }); + + return pipe( + stopDatafeedsResponsePayloadRT.decode(stopDatafeedResponse), + fold(throwErrors(createPlainError), identity) + ); +}; + +export const deleteJobsRequestPayloadRT = rt.type({ + jobIds: rt.array(rt.string), +}); + +export type DeleteJobsRequestPayload = rt.TypeOf; + +export const deleteJobsResponsePayloadRT = rt.record( + rt.string, + rt.type({ + deleted: rt.boolean, + }) +); + +export type DeleteJobsResponsePayload = rt.TypeOf; + +export const getJobDeletionTasksResponsePayloadRT = rt.type({ + jobIds: rt.array(rt.string), +}); + +export const stopDatafeedsRequestPayloadRT = rt.type({ + datafeedIds: rt.array(rt.string), +}); + +export const stopDatafeedsResponsePayloadRT = rt.record( + rt.string, + rt.type({ + stopped: rt.boolean, + }) +); diff --git a/x-pack/plugins/infra/public/containers/ml/api/ml_get_jobs_summary_api.ts b/x-pack/plugins/infra/public/containers/ml/api/ml_get_jobs_summary_api.ts new file mode 100644 index 0000000000000..3fddb63f69791 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/ml/api/ml_get_jobs_summary_api.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as rt from 'io-ts'; +import { npStart } from '../../../legacy_singletons'; + +import { getJobId, jobCustomSettingsRT } from '../../../../common/infra_ml'; +import { createPlainError, throwErrors } from '../../../../common/runtime_types'; + +export const callJobsSummaryAPI = async ( + spaceId: string, + sourceId: string, + jobTypes: JobType[] +) => { + const response = await npStart.http.fetch('/api/ml/jobs/jobs_summary', { + method: 'POST', + body: JSON.stringify( + fetchJobStatusRequestPayloadRT.encode({ + jobIds: jobTypes.map((jobType) => getJobId(spaceId, sourceId, jobType)), + }) + ), + }); + return pipe( + fetchJobStatusResponsePayloadRT.decode(response), + fold(throwErrors(createPlainError), identity) + ); +}; + +export const fetchJobStatusRequestPayloadRT = rt.type({ + jobIds: rt.array(rt.string), +}); + +export type FetchJobStatusRequestPayload = rt.TypeOf; + +const datafeedStateRT = rt.keyof({ + started: null, + stopped: null, + stopping: null, + '': null, +}); + +const jobStateRT = rt.keyof({ + closed: null, + closing: null, + deleting: null, + failed: null, + opened: null, + opening: null, +}); + +const jobCategorizationStatusRT = rt.keyof({ + ok: null, + warn: null, +}); + +const jobModelSizeStatsRT = rt.type({ + categorization_status: jobCategorizationStatusRT, + categorized_doc_count: rt.number, + dead_category_count: rt.number, + frequent_category_count: rt.number, + rare_category_count: rt.number, + total_category_count: rt.number, +}); + +export type JobModelSizeStats = rt.TypeOf; + +export const jobSummaryRT = rt.intersection([ + rt.type({ + id: rt.string, + jobState: jobStateRT, + }), + rt.partial({ + datafeedIndices: rt.array(rt.string), + datafeedState: datafeedStateRT, + fullJob: rt.partial({ + custom_settings: jobCustomSettingsRT, + finished_time: rt.number, + model_size_stats: jobModelSizeStatsRT, + }), + }), +]); + +export type JobSummary = rt.TypeOf; + +export const fetchJobStatusResponsePayloadRT = rt.array(jobSummaryRT); + +export type FetchJobStatusResponsePayload = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/containers/ml/api/ml_get_module.ts b/x-pack/plugins/infra/public/containers/ml/api/ml_get_module.ts new file mode 100644 index 0000000000000..d492522c120a1 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/ml/api/ml_get_module.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as rt from 'io-ts'; +import { npStart } from '../../../legacy_singletons'; + +import { jobCustomSettingsRT } from '../../../../common/log_analysis'; +import { createPlainError, throwErrors } from '../../../../common/runtime_types'; + +export const callGetMlModuleAPI = async (moduleId: string) => { + const response = await npStart.http.fetch(`/api/ml/modules/get_module/${moduleId}`, { + method: 'GET', + }); + + return pipe( + getMlModuleResponsePayloadRT.decode(response), + fold(throwErrors(createPlainError), identity) + ); +}; + +const jobDefinitionRT = rt.type({ + id: rt.string, + config: rt.type({ + custom_settings: jobCustomSettingsRT, + }), +}); + +export type JobDefinition = rt.TypeOf; + +const getMlModuleResponsePayloadRT = rt.type({ + id: rt.string, + jobs: rt.array(jobDefinitionRT), +}); + +export type GetMlModuleResponsePayload = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/containers/ml/api/ml_setup_module_api.ts b/x-pack/plugins/infra/public/containers/ml/api/ml_setup_module_api.ts new file mode 100644 index 0000000000000..06b0e075387b0 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/ml/api/ml_setup_module_api.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as rt from 'io-ts'; +import { npStart } from '../../../legacy_singletons'; + +import { getJobIdPrefix, jobCustomSettingsRT } from '../../../../common/infra_ml'; +import { createPlainError, throwErrors } from '../../../../common/runtime_types'; + +export const callSetupMlModuleAPI = async ( + moduleId: string, + start: number | undefined, + end: number | undefined, + spaceId: string, + sourceId: string, + indexPattern: string, + jobOverrides: SetupMlModuleJobOverrides[] = [], + datafeedOverrides: SetupMlModuleDatafeedOverrides[] = [], + query?: object +) => { + const response = await npStart.http.fetch(`/api/ml/modules/setup/${moduleId}`, { + method: 'POST', + body: JSON.stringify( + setupMlModuleRequestPayloadRT.encode({ + start, + end, + indexPatternName: indexPattern, + prefix: getJobIdPrefix(spaceId, sourceId), + startDatafeed: true, + jobOverrides, + datafeedOverrides, + query, + }) + ), + }); + + return pipe( + setupMlModuleResponsePayloadRT.decode(response), + fold(throwErrors(createPlainError), identity) + ); +}; + +const setupMlModuleTimeParamsRT = rt.partial({ + start: rt.number, + end: rt.number, +}); + +const setupMlModuleJobOverridesRT = rt.type({ + job_id: rt.string, + custom_settings: jobCustomSettingsRT, +}); + +export type SetupMlModuleJobOverrides = rt.TypeOf; + +const setupMlModuleDatafeedOverridesRT = rt.object; + +export type SetupMlModuleDatafeedOverrides = rt.TypeOf; + +const setupMlModuleRequestParamsRT = rt.intersection([ + rt.strict({ + indexPatternName: rt.string, + prefix: rt.string, + startDatafeed: rt.boolean, + jobOverrides: rt.array(setupMlModuleJobOverridesRT), + datafeedOverrides: rt.array(setupMlModuleDatafeedOverridesRT), + }), + rt.exact( + rt.partial({ + query: rt.object, + }) + ), +]); + +const setupMlModuleRequestPayloadRT = rt.intersection([ + setupMlModuleTimeParamsRT, + setupMlModuleRequestParamsRT, +]); + +const setupErrorResponseRT = rt.type({ + msg: rt.string, +}); + +const datafeedSetupResponseRT = rt.intersection([ + rt.type({ + id: rt.string, + started: rt.boolean, + success: rt.boolean, + }), + rt.partial({ + error: setupErrorResponseRT, + }), +]); + +const jobSetupResponseRT = rt.intersection([ + rt.type({ + id: rt.string, + success: rt.boolean, + }), + rt.partial({ + error: setupErrorResponseRT, + }), +]); + +const setupMlModuleResponsePayloadRT = rt.type({ + datafeeds: rt.array(datafeedSetupResponseRT), + jobs: rt.array(jobSetupResponseRT), +}); + +export type SetupMlModuleResponsePayload = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx new file mode 100644 index 0000000000000..f4c90a459af6a --- /dev/null +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_capabilities.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import createContainer from 'constate'; +import { useMemo, useState, useEffect } from 'react'; +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { identity } from 'fp-ts/lib/function'; +import { useTrackedPromise } from '../../utils/use_tracked_promise'; +import { npStart } from '../../legacy_singletons'; +import { + getMlCapabilitiesResponsePayloadRT, + GetMlCapabilitiesResponsePayload, +} from './api/ml_api_types'; +import { throwErrors, createPlainError } from '../../../common/runtime_types'; + +export const useInfraMLCapabilities = () => { + const [mlCapabilities, setMlCapabilities] = useState( + initialMlCapabilities + ); + + const [fetchMlCapabilitiesRequest, fetchMlCapabilities] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + const rawResponse = await npStart.http.fetch('/api/ml/ml_capabilities'); + + return pipe( + getMlCapabilitiesResponsePayloadRT.decode(rawResponse), + fold(throwErrors(createPlainError), identity) + ); + }, + onResolve: (response) => { + setMlCapabilities(response); + }, + }, + [] + ); + + useEffect(() => { + fetchMlCapabilities(); + }, [fetchMlCapabilities]); + + const isLoading = useMemo(() => fetchMlCapabilitiesRequest.state === 'pending', [ + fetchMlCapabilitiesRequest.state, + ]); + + const hasInfraMLSetupCapabilities = mlCapabilities.capabilities.canCreateJob; + const hasInfraMLReadCapabilities = mlCapabilities.capabilities.canGetJobs; + const hasInfraMLCapabilites = + mlCapabilities.isPlatinumOrTrialLicense && mlCapabilities.mlFeatureEnabledInSpace; + + return { + hasInfraMLCapabilites, + hasInfraMLReadCapabilities, + hasInfraMLSetupCapabilities, + isLoading, + }; +}; + +export const [InfraMLCapabilitiesProvider, useInfraMLCapabilitiesContext] = createContainer( + useInfraMLCapabilities +); + +const initialMlCapabilities = { + capabilities: { + canGetJobs: false, + canCreateJob: false, + canDeleteJob: false, + canOpenJob: false, + canCloseJob: false, + canForecastJob: false, + canGetDatafeeds: false, + canStartStopDatafeed: false, + canUpdateJob: false, + canUpdateDatafeed: false, + canPreviewDatafeed: false, + canGetCalendars: false, + canCreateCalendar: false, + canDeleteCalendar: false, + canGetFilters: false, + canCreateFilter: false, + canDeleteFilter: false, + canFindFileStructure: false, + canGetDataFrameJobs: false, + canDeleteDataFrameJob: false, + canPreviewDataFrameJob: false, + canCreateDataFrameJob: false, + canStartStopDataFrameJob: false, + }, + isPlatinumOrTrialLicense: false, + mlFeatureEnabledInSpace: false, + upgradeInProgress: false, +}; diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_cleanup.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_cleanup.tsx new file mode 100644 index 0000000000000..736982c8043b1 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_cleanup.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getJobId } from '../../../common/infra_ml'; +import { callDeleteJobs, callGetJobDeletionTasks, callStopDatafeeds } from './api/ml_cleanup'; + +export const cleanUpJobsAndDatafeeds = async ( + spaceId: string, + sourceId: string, + jobTypes: JobType[] +) => { + try { + await callStopDatafeeds(spaceId, sourceId, jobTypes); + } catch (err) { + // Proceed only if datafeed has been deleted or didn't exist in the first place + if (err?.res?.status !== 404) { + throw err; + } + } + + return await deleteJobs(spaceId, sourceId, jobTypes); +}; + +const deleteJobs = async ( + spaceId: string, + sourceId: string, + jobTypes: JobType[] +) => { + const deleteJobsResponse = await callDeleteJobs(spaceId, sourceId, jobTypes); + await waitUntilJobsAreDeleted(spaceId, sourceId, jobTypes); + return deleteJobsResponse; +}; + +const waitUntilJobsAreDeleted = async ( + spaceId: string, + sourceId: string, + jobTypes: JobType[] +) => { + const moduleJobIds = jobTypes.map((jobType) => getJobId(spaceId, sourceId, jobType)); + while (true) { + const { jobIds: jobIdsBeingDeleted } = await callGetJobDeletionTasks(); + const needToWait = jobIdsBeingDeleted.some((jobId) => moduleJobIds.includes(jobId)); + + if (needToWait) { + await timeout(1000); + } else { + return true; + } + } +}; + +const timeout = (ms: number) => new Promise((res) => setTimeout(res, ms)); diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx new file mode 100644 index 0000000000000..349541d108f5e --- /dev/null +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useMemo } from 'react'; +import { DatasetFilter } from '../../../common/infra_ml'; +import { useTrackedPromise } from '../../utils/use_tracked_promise'; +import { useModuleStatus } from './infra_ml_module_status'; +import { ModuleDescriptor, ModuleSourceConfiguration } from './infra_ml_module_types'; + +export const useInfraMLModule = ({ + sourceConfiguration, + moduleDescriptor, +}: { + sourceConfiguration: ModuleSourceConfiguration; + moduleDescriptor: ModuleDescriptor; +}) => { + const { spaceId, sourceId, timestampField } = sourceConfiguration; + const [moduleStatus, dispatchModuleStatus] = useModuleStatus(moduleDescriptor.jobTypes); + + const [, fetchJobStatus] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + dispatchModuleStatus({ type: 'fetchingJobStatuses' }); + return await moduleDescriptor.getJobSummary(spaceId, sourceId); + }, + onResolve: (jobResponse) => { + dispatchModuleStatus({ + type: 'fetchedJobStatuses', + payload: jobResponse, + spaceId, + sourceId, + }); + }, + onReject: () => { + dispatchModuleStatus({ type: 'failedFetchingJobStatuses' }); + }, + }, + [spaceId, sourceId] + ); + + const [, setUpModule] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async ( + selectedIndices: string[], + start: number | undefined, + end: number | undefined, + datasetFilter: DatasetFilter, + partitionField?: string + ) => { + dispatchModuleStatus({ type: 'startedSetup' }); + const setupResult = await moduleDescriptor.setUpModule( + start, + end, + datasetFilter, + { + indices: selectedIndices, + sourceId, + spaceId, + timestampField, + }, + partitionField + ); + const jobSummaries = await moduleDescriptor.getJobSummary(spaceId, sourceId); + return { setupResult, jobSummaries }; + }, + onResolve: ({ setupResult: { datafeeds, jobs }, jobSummaries }) => { + dispatchModuleStatus({ + type: 'finishedSetup', + datafeedSetupResults: datafeeds, + jobSetupResults: jobs, + jobSummaries, + spaceId, + sourceId, + }); + }, + onReject: () => { + dispatchModuleStatus({ type: 'failedSetup' }); + }, + }, + [moduleDescriptor.setUpModule, spaceId, sourceId, timestampField] + ); + + const [cleanUpModuleRequest, cleanUpModule] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return await moduleDescriptor.cleanUpModule(spaceId, sourceId); + }, + }, + [spaceId, sourceId] + ); + + const isCleaningUp = useMemo(() => cleanUpModuleRequest.state === 'pending', [ + cleanUpModuleRequest.state, + ]); + + const cleanUpAndSetUpModule = useCallback( + ( + selectedIndices: string[], + start: number | undefined, + end: number | undefined, + datasetFilter: DatasetFilter, + partitionField?: string + ) => { + dispatchModuleStatus({ type: 'startedSetup' }); + cleanUpModule() + .then(() => { + setUpModule(selectedIndices, start, end, datasetFilter, partitionField); + }) + .catch(() => { + dispatchModuleStatus({ type: 'failedSetup' }); + }); + }, + [cleanUpModule, dispatchModuleStatus, setUpModule] + ); + + const viewResults = useCallback(() => { + dispatchModuleStatus({ type: 'viewedResults' }); + }, [dispatchModuleStatus]); + + const jobIds = useMemo(() => moduleDescriptor.getJobIds(spaceId, sourceId), [ + moduleDescriptor, + spaceId, + sourceId, + ]); + + return { + cleanUpAndSetUpModule, + cleanUpModule, + fetchJobStatus, + isCleaningUp, + jobIds, + jobStatus: moduleStatus.jobStatus, + jobSummaries: moduleStatus.jobSummaries, + lastSetupErrorMessages: moduleStatus.lastSetupErrorMessages, + moduleDescriptor, + setUpModule, + setupStatus: moduleStatus.setupStatus, + sourceConfiguration, + viewResults, + }; +}; diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_configuration.ts b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_configuration.ts new file mode 100644 index 0000000000000..2d90f5d531010 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_configuration.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo } from 'react'; +import { JobSummary } from './api/ml_get_jobs_summary_api'; +import { ModuleDescriptor, ModuleSourceConfiguration } from './infra_ml_module_types'; + +export const useInfraMLModuleConfiguration = ({ + moduleDescriptor, + sourceConfiguration, +}: { + moduleDescriptor: ModuleDescriptor; + sourceConfiguration: ModuleSourceConfiguration; +}) => { + const getIsJobConfigurationOutdated = useMemo( + () => isJobConfigurationOutdated(moduleDescriptor, sourceConfiguration), + [sourceConfiguration, moduleDescriptor] + ); + + return { + getIsJobConfigurationOutdated, + }; +}; + +export const isJobConfigurationOutdated = ( + { bucketSpan }: ModuleDescriptor, + currentSourceConfiguration: ModuleSourceConfiguration +) => (jobSummary: JobSummary): boolean => { + if (!jobSummary.fullJob || !jobSummary.fullJob.custom_settings) { + return false; + } + + const jobConfiguration = jobSummary.fullJob.custom_settings.metrics_source_config; + + return !( + jobConfiguration && + jobConfiguration.bucketSpan === bucketSpan && + jobConfiguration.indexPattern && + isSubset( + new Set(jobConfiguration.indexPattern.split(',')), + new Set(currentSourceConfiguration.indices) + ) && + jobConfiguration.timestampField === currentSourceConfiguration.timestampField + ); +}; + +const isSubset = (subset: Set, superset: Set) => { + return Array.from(subset).every((subsetElement) => superset.has(subsetElement)); +}; diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_definition.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_definition.tsx new file mode 100644 index 0000000000000..3c7ffcfd4a4e2 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_definition.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useMemo, useState } from 'react'; +import { getJobId } from '../../../common/log_analysis'; +import { useTrackedPromise } from '../../utils/use_tracked_promise'; +import { JobSummary } from './api/ml_get_jobs_summary_api'; +import { GetMlModuleResponsePayload, JobDefinition } from './api/ml_get_module'; +import { ModuleDescriptor, ModuleSourceConfiguration } from './infra_ml_module_types'; + +export const useInfraMLModuleDefinition = ({ + sourceConfiguration: { spaceId, sourceId }, + moduleDescriptor, +}: { + sourceConfiguration: ModuleSourceConfiguration; + moduleDescriptor: ModuleDescriptor; +}) => { + const [moduleDefinition, setModuleDefinition] = useState< + GetMlModuleResponsePayload | undefined + >(); + + const jobDefinitionByJobId = useMemo( + () => + moduleDefinition + ? moduleDefinition.jobs.reduce>( + (accumulatedJobDefinitions, jobDefinition) => ({ + ...accumulatedJobDefinitions, + [getJobId(spaceId, sourceId, jobDefinition.id)]: jobDefinition, + }), + {} + ) + : {}, + [moduleDefinition, sourceId, spaceId] + ); + + const [fetchModuleDefinitionRequest, fetchModuleDefinition] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return await moduleDescriptor.getModuleDefinition(); + }, + onResolve: (response) => { + setModuleDefinition(response); + }, + onReject: () => { + setModuleDefinition(undefined); + }, + }, + [moduleDescriptor.getModuleDefinition, spaceId, sourceId] + ); + + const getIsJobDefinitionOutdated = useCallback( + (jobSummary: JobSummary): boolean => { + const jobDefinition: JobDefinition | undefined = jobDefinitionByJobId[jobSummary.id]; + + if (jobDefinition == null) { + return false; + } + + const currentRevision = jobDefinition?.config.custom_settings.job_revision; + return (jobSummary.fullJob?.custom_settings?.job_revision ?? 0) < (currentRevision ?? 0); + }, + [jobDefinitionByJobId] + ); + + return { + fetchModuleDefinition, + fetchModuleDefinitionRequestState: fetchModuleDefinitionRequest.state, + getIsJobDefinitionOutdated, + jobDefinitionByJobId, + moduleDefinition, + }; +}; diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_status.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_status.tsx new file mode 100644 index 0000000000000..63d479546b44f --- /dev/null +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_status.tsx @@ -0,0 +1,268 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useReducer } from 'react'; + +import { + JobStatus, + getDatafeedId, + getJobId, + isJobStatusWithResults, + SetupStatus, +} from '../../../common/infra_ml'; +import { FetchJobStatusResponsePayload, JobSummary } from './api/ml_get_jobs_summary_api'; +import { SetupMlModuleResponsePayload } from './api/ml_setup_module_api'; +import { MandatoryProperty } from '../../../common/utility_types'; + +interface StatusReducerState { + jobStatus: Record; + jobSummaries: JobSummary[]; + lastSetupErrorMessages: string[]; + setupStatus: SetupStatus; +} + +type StatusReducerAction = + | { type: 'startedSetup' } + | { + type: 'finishedSetup'; + sourceId: string; + spaceId: string; + jobSetupResults: SetupMlModuleResponsePayload['jobs']; + jobSummaries: FetchJobStatusResponsePayload; + datafeedSetupResults: SetupMlModuleResponsePayload['datafeeds']; + } + | { type: 'failedSetup' } + | { type: 'fetchingJobStatuses' } + | { + type: 'fetchedJobStatuses'; + spaceId: string; + sourceId: string; + payload: FetchJobStatusResponsePayload; + } + | { type: 'failedFetchingJobStatuses' } + | { type: 'viewedResults' }; + +const createInitialState = ({ + jobTypes, +}: { + jobTypes: JobType[]; +}): StatusReducerState => ({ + jobStatus: jobTypes.reduce( + (accumulatedJobStatus, jobType) => ({ + ...accumulatedJobStatus, + [jobType]: 'unknown', + }), + {} as Record + ), + jobSummaries: [], + lastSetupErrorMessages: [], + setupStatus: { type: 'initializing' }, +}); + +const createStatusReducer = (jobTypes: JobType[]) => ( + state: StatusReducerState, + action: StatusReducerAction +): StatusReducerState => { + switch (action.type) { + case 'startedSetup': { + return { + ...state, + jobStatus: jobTypes.reduce( + (accumulatedJobStatus, jobType) => ({ + ...accumulatedJobStatus, + [jobType]: 'initializing', + }), + {} as Record + ), + setupStatus: { type: 'pending' }, + }; + } + case 'finishedSetup': { + const { datafeedSetupResults, jobSetupResults, jobSummaries, spaceId, sourceId } = action; + const nextJobStatus = jobTypes.reduce( + (accumulatedJobStatus, jobType) => ({ + ...accumulatedJobStatus, + [jobType]: + hasSuccessfullyCreatedJob(getJobId(spaceId, sourceId, jobType))(jobSetupResults) && + hasSuccessfullyStartedDatafeed(getDatafeedId(spaceId, sourceId, jobType))( + datafeedSetupResults + ) + ? 'started' + : 'failed', + }), + {} as Record + ); + const nextSetupStatus: SetupStatus = Object.values(nextJobStatus).every( + (jobState) => jobState === 'started' + ) + ? { type: 'succeeded' } + : { + type: 'failed', + reasons: [ + ...Object.values(datafeedSetupResults) + .filter(hasError) + .map((datafeed) => datafeed.error.msg), + ...Object.values(jobSetupResults) + .filter(hasError) + .map((job) => job.error.msg), + ], + }; + + return { + ...state, + jobStatus: nextJobStatus, + jobSummaries, + setupStatus: nextSetupStatus, + }; + } + case 'failedSetup': { + return { + ...state, + jobStatus: jobTypes.reduce( + (accumulatedJobStatus, jobType) => ({ + ...accumulatedJobStatus, + [jobType]: 'failed', + }), + {} as Record + ), + setupStatus: { type: 'failed', reasons: ['unknown'] }, + }; + } + case 'fetchingJobStatuses': { + return { + ...state, + setupStatus: + state.setupStatus.type === 'unknown' ? { type: 'initializing' } : state.setupStatus, + }; + } + case 'fetchedJobStatuses': { + const { payload: jobSummaries, spaceId, sourceId } = action; + const { setupStatus } = state; + const nextJobStatus = jobTypes.reduce( + (accumulatedJobStatus, jobType) => ({ + ...accumulatedJobStatus, + [jobType]: getJobStatus(getJobId(spaceId, sourceId, jobType))(jobSummaries), + }), + {} as Record + ); + const nextSetupStatus = getSetupStatus(nextJobStatus)(setupStatus); + + return { + ...state, + jobSummaries, + jobStatus: nextJobStatus, + setupStatus: nextSetupStatus, + }; + } + case 'failedFetchingJobStatuses': { + return { + ...state, + setupStatus: { type: 'unknown' }, + jobStatus: jobTypes.reduce( + (accumulatedJobStatus, jobType) => ({ + ...accumulatedJobStatus, + [jobType]: 'unknown', + }), + {} as Record + ), + }; + } + case 'viewedResults': { + return { + ...state, + setupStatus: { type: 'skipped', newlyCreated: true }, + }; + } + default: { + return state; + } + } +}; + +const hasSuccessfullyCreatedJob = (jobId: string) => ( + jobSetupResponses: SetupMlModuleResponsePayload['jobs'] +) => + jobSetupResponses.filter( + (jobSetupResponse) => + jobSetupResponse.id === jobId && jobSetupResponse.success && !jobSetupResponse.error + ).length > 0; + +const hasSuccessfullyStartedDatafeed = (datafeedId: string) => ( + datafeedSetupResponses: SetupMlModuleResponsePayload['datafeeds'] +) => + datafeedSetupResponses.filter( + (datafeedSetupResponse) => + datafeedSetupResponse.id === datafeedId && + datafeedSetupResponse.success && + datafeedSetupResponse.started && + !datafeedSetupResponse.error + ).length > 0; + +const getJobStatus = (jobId: string) => ( + jobSummaries: FetchJobStatusResponsePayload +): JobStatus => { + return ( + jobSummaries + .filter((jobSummary) => jobSummary.id === jobId) + .map( + (jobSummary): JobStatus => { + if (jobSummary.jobState === 'failed' || jobSummary.datafeedState === '') { + return 'failed'; + } else if ( + jobSummary.jobState === 'closed' && + jobSummary.datafeedState === 'stopped' && + jobSummary.fullJob && + jobSummary.fullJob.finished_time != null + ) { + return 'finished'; + } else if ( + jobSummary.jobState === 'closed' || + jobSummary.jobState === 'closing' || + jobSummary.datafeedState === 'stopped' + ) { + return 'stopped'; + } else if (jobSummary.jobState === 'opening') { + return 'initializing'; + } else if (jobSummary.jobState === 'opened' && jobSummary.datafeedState === 'started') { + return 'started'; + } + + return 'unknown'; + } + )[0] || 'missing' + ); +}; + +const getSetupStatus = (everyJobStatus: Record) => ( + previousSetupStatus: SetupStatus +): SetupStatus => { + return Object.entries(everyJobStatus).reduce( + (setupStatus, [, jobStatus]) => { + if (jobStatus === 'missing') { + return { type: 'required' }; + } else if (setupStatus.type === 'required' || setupStatus.type === 'succeeded') { + return setupStatus; + } else if (setupStatus.type === 'skipped' || isJobStatusWithResults(jobStatus)) { + return { + type: 'skipped', + // preserve newlyCreated status + newlyCreated: setupStatus.type === 'skipped' && setupStatus.newlyCreated, + }; + } + + return setupStatus; + }, + previousSetupStatus + ); +}; + +const hasError = ( + value: Value +): value is MandatoryProperty => value.error != null; + +export const useModuleStatus = (jobTypes: JobType[]) => { + return useReducer(createStatusReducer(jobTypes), { jobTypes }, createInitialState); +}; diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts new file mode 100644 index 0000000000000..a9f2671de8259 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ValidateLogEntryDatasetsResponsePayload, + ValidationIndicesResponsePayload, +} from '../../../common/http_api/log_analysis'; +import { DatasetFilter } from '../../../common/infra_ml'; +import { DeleteJobsResponsePayload } from './api/ml_cleanup'; +import { FetchJobStatusResponsePayload } from './api/ml_get_jobs_summary_api'; +import { GetMlModuleResponsePayload } from './api/ml_get_module'; +import { SetupMlModuleResponsePayload } from './api/ml_setup_module_api'; + +export { JobModelSizeStats, JobSummary } from './api/ml_get_jobs_summary_api'; + +export interface ModuleDescriptor { + moduleId: string; + moduleName: string; + moduleDescription: string; + jobTypes: JobType[]; + bucketSpan: number; + getJobIds: (spaceId: string, sourceId: string) => Record; + getJobSummary: (spaceId: string, sourceId: string) => Promise; + getModuleDefinition: () => Promise; + setUpModule: ( + start: number | undefined, + end: number | undefined, + datasetFilter: DatasetFilter, + sourceConfiguration: ModuleSourceConfiguration, + partitionField?: string + ) => Promise; + cleanUpModule: (spaceId: string, sourceId: string) => Promise; + validateSetupIndices: ( + indices: string[], + timestampField: string + ) => Promise; + validateSetupDatasets: ( + indices: string[], + timestampField: string, + startTime: number, + endTime: number + ) => Promise; +} + +export interface ModuleSourceConfiguration { + indices: string[]; + sourceId: string; + spaceId: string; + timestampField: string; +} + +interface ManyCategoriesWarningReason { + type: 'manyCategories'; + categoriesDocumentRatio: number; +} + +interface ManyDeadCategoriesWarningReason { + type: 'manyDeadCategories'; + deadCategoriesRatio: number; +} + +interface ManyRareCategoriesWarningReason { + type: 'manyRareCategories'; + rareCategoriesRatio: number; +} + +interface NoFrequentCategoriesWarningReason { + type: 'noFrequentCategories'; +} + +interface SingleCategoryWarningReason { + type: 'singleCategory'; +} + +export type CategoryQualityWarningReason = + | ManyCategoriesWarningReason + | ManyDeadCategoriesWarningReason + | ManyRareCategoriesWarningReason + | NoFrequentCategoriesWarningReason + | SingleCategoryWarningReason; + +export type CategoryQualityWarningReasonType = CategoryQualityWarningReason['type']; + +export interface CategoryQualityWarning { + type: 'categoryQualityWarning'; + jobId: string; + reasons: CategoryQualityWarningReason[]; +} + +export type QualityWarning = CategoryQualityWarning; diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_setup_state.ts b/x-pack/plugins/infra/public/containers/ml/infra_ml_setup_state.ts new file mode 100644 index 0000000000000..0dfe3b301f240 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_setup_state.ts @@ -0,0 +1,289 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEqual } from 'lodash'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { usePrevious } from 'react-use'; +import { + combineDatasetFilters, + DatasetFilter, + filterDatasetFilter, + isExampleDataIndex, +} from '../../../common/infra_ml'; +import { + AvailableIndex, + ValidationIndicesError, + ValidationUIError, +} from '../../components/logging/log_analysis_setup/initial_configuration_step'; +import { useTrackedPromise } from '../../utils/use_tracked_promise'; +import { ModuleDescriptor, ModuleSourceConfiguration } from './infra_ml_module_types'; + +type SetupHandler = ( + indices: string[], + startTime: number | undefined, + endTime: number | undefined, + datasetFilter: DatasetFilter +) => void; + +interface AnalysisSetupStateArguments { + cleanUpAndSetUpModule: SetupHandler; + moduleDescriptor: ModuleDescriptor; + setUpModule: SetupHandler; + sourceConfiguration: ModuleSourceConfiguration; +} + +const fourWeeksInMs = 86400000 * 7 * 4; + +export const useAnalysisSetupState = ({ + cleanUpAndSetUpModule, + moduleDescriptor: { validateSetupDatasets, validateSetupIndices }, + setUpModule, + sourceConfiguration, +}: AnalysisSetupStateArguments) => { + const [startTime, setStartTime] = useState(Date.now() - fourWeeksInMs); + const [endTime, setEndTime] = useState(undefined); + + const isTimeRangeValid = useMemo( + () => (startTime != null && endTime != null ? startTime < endTime : true), + [endTime, startTime] + ); + + const [validatedIndices, setValidatedIndices] = useState( + sourceConfiguration.indices.map((indexName) => ({ + name: indexName, + validity: 'unknown' as const, + })) + ); + + const updateIndicesWithValidationErrors = useCallback( + (validationErrors: ValidationIndicesError[]) => + setValidatedIndices((availableIndices) => + availableIndices.map((previousAvailableIndex) => { + const indexValiationErrors = validationErrors.filter( + ({ index }) => index === previousAvailableIndex.name + ); + + if (indexValiationErrors.length > 0) { + return { + validity: 'invalid', + name: previousAvailableIndex.name, + errors: indexValiationErrors, + }; + } else if (previousAvailableIndex.validity === 'valid') { + return { + ...previousAvailableIndex, + validity: 'valid', + errors: [], + }; + } else { + return { + validity: 'valid', + name: previousAvailableIndex.name, + isSelected: !isExampleDataIndex(previousAvailableIndex.name), + availableDatasets: [], + datasetFilter: { + type: 'includeAll' as const, + }, + }; + } + }) + ), + [] + ); + + const updateIndicesWithAvailableDatasets = useCallback( + (availableDatasets: Array<{ indexName: string; datasets: string[] }>) => + setValidatedIndices((availableIndices) => + availableIndices.map((previousAvailableIndex) => { + if (previousAvailableIndex.validity !== 'valid') { + return previousAvailableIndex; + } + + const availableDatasetsForIndex = availableDatasets.filter( + ({ indexName }) => indexName === previousAvailableIndex.name + ); + const newAvailableDatasets = availableDatasetsForIndex.flatMap( + ({ datasets }) => datasets + ); + + // filter out datasets that have disappeared if this index' datasets were updated + const newDatasetFilter: DatasetFilter = + availableDatasetsForIndex.length > 0 + ? filterDatasetFilter(previousAvailableIndex.datasetFilter, (dataset) => + newAvailableDatasets.includes(dataset) + ) + : previousAvailableIndex.datasetFilter; + + return { + ...previousAvailableIndex, + availableDatasets: newAvailableDatasets, + datasetFilter: newDatasetFilter, + }; + }) + ), + [] + ); + + const validIndexNames = useMemo( + () => validatedIndices.filter((index) => index.validity === 'valid').map((index) => index.name), + [validatedIndices] + ); + + const selectedIndexNames = useMemo( + () => + validatedIndices + .filter((index) => index.validity === 'valid' && index.isSelected) + .map((i) => i.name), + [validatedIndices] + ); + + const datasetFilter = useMemo( + () => + validatedIndices + .flatMap((validatedIndex) => + validatedIndex.validity === 'valid' + ? validatedIndex.datasetFilter + : { type: 'includeAll' as const } + ) + .reduce(combineDatasetFilters, { type: 'includeAll' as const }), + [validatedIndices] + ); + + const [validateIndicesRequest, validateIndices] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + return await validateSetupIndices( + sourceConfiguration.indices, + sourceConfiguration.timestampField + ); + }, + onResolve: ({ data: { errors } }) => { + updateIndicesWithValidationErrors(errors); + }, + onReject: () => { + setValidatedIndices([]); + }, + }, + [sourceConfiguration.indices, sourceConfiguration.timestampField] + ); + + const [validateDatasetsRequest, validateDatasets] = useTrackedPromise( + { + cancelPreviousOn: 'resolution', + createPromise: async () => { + if (validIndexNames.length === 0) { + return { data: { datasets: [] } }; + } + + return await validateSetupDatasets( + validIndexNames, + sourceConfiguration.timestampField, + startTime ?? 0, + endTime ?? Date.now() + ); + }, + onResolve: ({ data: { datasets } }) => { + updateIndicesWithAvailableDatasets(datasets); + }, + }, + [validIndexNames, sourceConfiguration.timestampField, startTime, endTime] + ); + + const setUp = useCallback(() => { + return setUpModule(selectedIndexNames, startTime, endTime, datasetFilter); + }, [setUpModule, selectedIndexNames, startTime, endTime, datasetFilter]); + + const cleanUpAndSetUp = useCallback(() => { + return cleanUpAndSetUpModule(selectedIndexNames, startTime, endTime, datasetFilter); + }, [cleanUpAndSetUpModule, selectedIndexNames, startTime, endTime, datasetFilter]); + + const isValidating = useMemo( + () => validateIndicesRequest.state === 'pending' || validateDatasetsRequest.state === 'pending', + [validateDatasetsRequest.state, validateIndicesRequest.state] + ); + + const validationErrors = useMemo(() => { + if (isValidating) { + return []; + } + + return [ + // validate request status + ...(validateIndicesRequest.state === 'rejected' || + validateDatasetsRequest.state === 'rejected' + ? [{ error: 'NETWORK_ERROR' as const }] + : []), + // validation request results + ...validatedIndices.reduce((errors, index) => { + return index.validity === 'invalid' && selectedIndexNames.includes(index.name) + ? [...errors, ...index.errors] + : errors; + }, []), + // index count + ...(selectedIndexNames.length === 0 ? [{ error: 'TOO_FEW_SELECTED_INDICES' as const }] : []), + // time range + ...(!isTimeRangeValid ? [{ error: 'INVALID_TIME_RANGE' as const }] : []), + ]; + }, [ + isValidating, + validateIndicesRequest.state, + validateDatasetsRequest.state, + validatedIndices, + selectedIndexNames, + isTimeRangeValid, + ]); + + const prevStartTime = usePrevious(startTime); + const prevEndTime = usePrevious(endTime); + const prevValidIndexNames = usePrevious(validIndexNames); + + useEffect(() => { + if (!isTimeRangeValid) { + return; + } + + validateIndices(); + }, [isTimeRangeValid, validateIndices]); + + useEffect(() => { + if (!isTimeRangeValid) { + return; + } + + if ( + startTime !== prevStartTime || + endTime !== prevEndTime || + !isEqual(validIndexNames, prevValidIndexNames) + ) { + validateDatasets(); + } + }, [ + endTime, + isTimeRangeValid, + prevEndTime, + prevStartTime, + prevValidIndexNames, + startTime, + validIndexNames, + validateDatasets, + ]); + + return { + cleanUpAndSetUp, + datasetFilter, + endTime, + isValidating, + selectedIndexNames, + setEndTime, + setStartTime, + setUp, + startTime, + validatedIndices, + setValidatedIndices, + validationErrors, + }; +}; diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module.tsx b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module.tsx new file mode 100644 index 0000000000000..9c065f3e91bc4 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import createContainer from 'constate'; +import { useMemo } from 'react'; +import { useInfraMLModule } from '../../infra_ml_module'; +import { useInfraMLModuleConfiguration } from '../../infra_ml_module_configuration'; +import { useInfraMLModuleDefinition } from '../../infra_ml_module_definition'; +import { ModuleSourceConfiguration } from '../../infra_ml_module_types'; +import { metricHostsModule } from './module_descriptor'; + +export const useMetricHostsModule = ({ + indexPattern, + sourceId, + spaceId, + timestampField, +}: { + indexPattern: string; + sourceId: string; + spaceId: string; + timestampField: string; +}) => { + const sourceConfiguration: ModuleSourceConfiguration = useMemo( + () => ({ + indices: indexPattern.split(','), + sourceId, + spaceId, + timestampField, + }), + [indexPattern, sourceId, spaceId, timestampField] + ); + + const infraMLModule = useInfraMLModule({ + moduleDescriptor: metricHostsModule, + sourceConfiguration, + }); + + const { getIsJobConfigurationOutdated } = useInfraMLModuleConfiguration({ + sourceConfiguration, + moduleDescriptor: metricHostsModule, + }); + + const { fetchModuleDefinition, getIsJobDefinitionOutdated } = useInfraMLModuleDefinition({ + sourceConfiguration, + moduleDescriptor: metricHostsModule, + }); + + const hasOutdatedJobConfigurations = useMemo( + () => infraMLModule.jobSummaries.some(getIsJobConfigurationOutdated), + [getIsJobConfigurationOutdated, infraMLModule.jobSummaries] + ); + + const hasOutdatedJobDefinitions = useMemo( + () => infraMLModule.jobSummaries.some(getIsJobDefinitionOutdated), + [getIsJobDefinitionOutdated, infraMLModule.jobSummaries] + ); + + const hasStoppedJobs = useMemo( + () => + Object.values(infraMLModule.jobStatus).some( + (currentJobStatus) => currentJobStatus === 'stopped' + ), + [infraMLModule.jobStatus] + ); + + return { + ...infraMLModule, + fetchModuleDefinition, + hasOutdatedJobConfigurations, + hasOutdatedJobDefinitions, + hasStoppedJobs, + }; +}; + +export const [MetricHostsModuleProvider, useMetricHostsModuleContext] = createContainer( + useMetricHostsModule +); diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts new file mode 100644 index 0000000000000..cec87fb1144e3 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { ModuleDescriptor, ModuleSourceConfiguration } from '../../infra_ml_module_types'; +import { cleanUpJobsAndDatafeeds } from '../../infra_ml_cleanup'; +import { callJobsSummaryAPI } from '../../api/ml_get_jobs_summary_api'; +import { callGetMlModuleAPI } from '../../api/ml_get_module'; +import { callSetupMlModuleAPI } from '../../api/ml_setup_module_api'; +import { callValidateIndicesAPI } from '../../../logs/log_analysis/api/validate_indices'; +import { callValidateDatasetsAPI } from '../../../logs/log_analysis/api/validate_datasets'; +import { + metricsHostsJobTypes, + getJobId, + MetricsHostsJobType, + DatasetFilter, + bucketSpan, + partitionField, +} from '../../../../../common/infra_ml'; + +const moduleId = 'metrics_ui_hosts'; +const moduleName = i18n.translate('xpack.infra.ml.metricsModuleName', { + defaultMessage: 'Metrics anomanly detection', +}); +const moduleDescription = i18n.translate('xpack.infra.ml.metricsHostModuleDescription', { + defaultMessage: 'Use Machine Learning to automatically detect anomalous log entry rates.', +}); + +const getJobIds = (spaceId: string, sourceId: string) => + metricsHostsJobTypes.reduce( + (accumulatedJobIds, jobType) => ({ + ...accumulatedJobIds, + [jobType]: getJobId(spaceId, sourceId, jobType), + }), + {} as Record + ); + +const getJobSummary = async (spaceId: string, sourceId: string) => { + const response = await callJobsSummaryAPI(spaceId, sourceId, metricsHostsJobTypes); + const jobIds = Object.values(getJobIds(spaceId, sourceId)); + + return response.filter((jobSummary) => jobIds.includes(jobSummary.id)); +}; + +const getModuleDefinition = async () => { + return await callGetMlModuleAPI(moduleId); +}; + +const setUpModule = async ( + start: number | undefined, + end: number | undefined, + datasetFilter: DatasetFilter, + { spaceId, sourceId, indices, timestampField }: ModuleSourceConfiguration, + pField?: string +) => { + const indexNamePattern = indices.join(','); + const jobIds = ['hosts_memory_usage', 'hosts_network_in', 'hosts_network_out']; + const jobOverrides = jobIds.map((id) => ({ + job_id: id, + data_description: { + time_field: timestampField, + }, + custom_settings: { + metrics_source_config: { + indexPattern: indexNamePattern, + timestampField, + bucketSpan, + }, + }, + })); + + return callSetupMlModuleAPI( + moduleId, + start, + end, + spaceId, + sourceId, + indexNamePattern, + jobOverrides, + [] + ); +}; + +const cleanUpModule = async (spaceId: string, sourceId: string) => { + return await cleanUpJobsAndDatafeeds(spaceId, sourceId, metricsHostsJobTypes); +}; + +const validateSetupIndices = async (indices: string[], timestampField: string) => { + return await callValidateIndicesAPI(indices, [ + { + name: timestampField, + validTypes: ['date'], + }, + { + name: partitionField, + validTypes: ['keyword'], + }, + ]); +}; + +const validateSetupDatasets = async ( + indices: string[], + timestampField: string, + startTime: number, + endTime: number +) => { + return await callValidateDatasetsAPI(indices, timestampField, startTime, endTime); +}; + +export const metricHostsModule: ModuleDescriptor = { + moduleId, + moduleName, + moduleDescription, + jobTypes: metricsHostsJobTypes, + bucketSpan, + getJobIds, + getJobSummary, + getModuleDefinition, + setUpModule, + cleanUpModule, + validateSetupDatasets, + validateSetupIndices, +}; diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module.tsx b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module.tsx new file mode 100644 index 0000000000000..07c8ab02f17ee --- /dev/null +++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import createContainer from 'constate'; +import { useMemo } from 'react'; +import { useInfraMLModule } from '../../infra_ml_module'; +import { useInfraMLModuleConfiguration } from '../../infra_ml_module_configuration'; +import { useInfraMLModuleDefinition } from '../../infra_ml_module_definition'; +import { ModuleSourceConfiguration } from '../../infra_ml_module_types'; +import { metricHostsModule } from './module_descriptor'; + +export const useMetricK8sModule = ({ + indexPattern, + sourceId, + spaceId, + timestampField, +}: { + indexPattern: string; + sourceId: string; + spaceId: string; + timestampField: string; +}) => { + const sourceConfiguration: ModuleSourceConfiguration = useMemo( + () => ({ + indices: indexPattern.split(','), + sourceId, + spaceId, + timestampField, + }), + [indexPattern, sourceId, spaceId, timestampField] + ); + + const infraMLModule = useInfraMLModule({ + moduleDescriptor: metricHostsModule, + sourceConfiguration, + }); + + const { getIsJobConfigurationOutdated } = useInfraMLModuleConfiguration({ + sourceConfiguration, + moduleDescriptor: metricHostsModule, + }); + + const { fetchModuleDefinition, getIsJobDefinitionOutdated } = useInfraMLModuleDefinition({ + sourceConfiguration, + moduleDescriptor: metricHostsModule, + }); + + const hasOutdatedJobConfigurations = useMemo( + () => infraMLModule.jobSummaries.some(getIsJobConfigurationOutdated), + [getIsJobConfigurationOutdated, infraMLModule.jobSummaries] + ); + + const hasOutdatedJobDefinitions = useMemo( + () => infraMLModule.jobSummaries.some(getIsJobDefinitionOutdated), + [getIsJobDefinitionOutdated, infraMLModule.jobSummaries] + ); + + const hasStoppedJobs = useMemo( + () => + Object.values(infraMLModule.jobStatus).some( + (currentJobStatus) => currentJobStatus === 'stopped' + ), + [infraMLModule.jobStatus] + ); + + return { + ...infraMLModule, + fetchModuleDefinition, + hasOutdatedJobConfigurations, + hasOutdatedJobDefinitions, + hasStoppedJobs, + }; +}; + +export const [MetricK8sModuleProvider, useMetricK8sModuleContext] = createContainer( + useMetricK8sModule +); diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts new file mode 100644 index 0000000000000..cbcff1c307af6 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { ModuleDescriptor, ModuleSourceConfiguration } from '../../infra_ml_module_types'; +import { cleanUpJobsAndDatafeeds } from '../../infra_ml_cleanup'; +import { callJobsSummaryAPI } from '../../api/ml_get_jobs_summary_api'; +import { callGetMlModuleAPI } from '../../api/ml_get_module'; +import { callSetupMlModuleAPI } from '../../api/ml_setup_module_api'; +import { callValidateIndicesAPI } from '../../../logs/log_analysis/api/validate_indices'; +import { callValidateDatasetsAPI } from '../../../logs/log_analysis/api/validate_datasets'; +import { + metricsK8SJobTypes, + getJobId, + MetricK8sJobType, + DatasetFilter, + bucketSpan, + partitionField, +} from '../../../../../common/infra_ml'; + +const moduleId = 'metrics_ui_k8s'; +const moduleName = i18n.translate('xpack.infra.ml.metricsModuleName', { + defaultMessage: 'Metrics anomanly detection', +}); +const moduleDescription = i18n.translate('xpack.infra.ml.metricsHostModuleDescription', { + defaultMessage: 'Use Machine Learning to automatically detect anomalous log entry rates.', +}); + +const getJobIds = (spaceId: string, sourceId: string) => + metricsK8SJobTypes.reduce( + (accumulatedJobIds, jobType) => ({ + ...accumulatedJobIds, + [jobType]: getJobId(spaceId, sourceId, jobType), + }), + {} as Record + ); + +const getJobSummary = async (spaceId: string, sourceId: string) => { + const response = await callJobsSummaryAPI(spaceId, sourceId, metricsK8SJobTypes); + const jobIds = Object.values(getJobIds(spaceId, sourceId)); + + return response.filter((jobSummary) => jobIds.includes(jobSummary.id)); +}; + +const getModuleDefinition = async () => { + return await callGetMlModuleAPI(moduleId); +}; + +const setUpModule = async ( + start: number | undefined, + end: number | undefined, + datasetFilter: DatasetFilter, + { spaceId, sourceId, indices, timestampField }: ModuleSourceConfiguration, + pField?: string +) => { + const indexNamePattern = indices.join(','); + const jobIds = ['k8s_memory_usage', 'k8s_network_in', 'k8s_network_out']; + const jobOverrides = jobIds.map((id) => ({ + job_id: id, + analysis_config: { + bucket_span: `${bucketSpan}ms`, + }, + data_description: { + time_field: timestampField, + }, + custom_settings: { + metrics_source_config: { + indexPattern: indexNamePattern, + timestampField, + bucketSpan, + }, + }, + })); + + return callSetupMlModuleAPI( + moduleId, + start, + end, + spaceId, + sourceId, + indexNamePattern, + jobOverrides, + [] + ); +}; + +const cleanUpModule = async (spaceId: string, sourceId: string) => { + return await cleanUpJobsAndDatafeeds(spaceId, sourceId, metricsK8SJobTypes); +}; + +const validateSetupIndices = async (indices: string[], timestampField: string) => { + return await callValidateIndicesAPI(indices, [ + { + name: timestampField, + validTypes: ['date'], + }, + { + name: partitionField, + validTypes: ['keyword'], + }, + ]); +}; + +const validateSetupDatasets = async ( + indices: string[], + timestampField: string, + startTime: number, + endTime: number +) => { + return await callValidateDatasetsAPI(indices, timestampField, startTime, endTime); +}; + +export const metricHostsModule: ModuleDescriptor = { + moduleId, + moduleName, + moduleDescription, + jobTypes: metricsK8SJobTypes, + bucketSpan, + getJobIds, + getJobSummary, + getModuleDefinition, + setUpModule, + cleanUpModule, + validateSetupDatasets, + validateSetupIndices, +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 3b3ed80f9e731..ac2c87248ae77 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -38,6 +38,8 @@ import { MetricsAlertDropdown } from '../../alerting/metric_threshold/components import { SavedView } from '../../containers/saved_view/saved_view'; import { SourceConfigurationFields } from '../../graphql/types'; import { AlertPrefillProvider } from '../../alerting/use_alert_prefill'; +import { InfraMLCapabilitiesProvider } from '../../containers/ml/infra_ml_capabilities'; +import { AnomalyDetectionFlyout } from './inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout'; const ADD_DATA_LABEL = i18n.translate('xpack.infra.metricsHeaderAddDataButtonLabel', { defaultMessage: 'Add data', @@ -55,110 +57,118 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { - - + + + - + -
+ - - - - - - - - - - - - {ADD_DATA_LABEL} - - - - - - - - ( - - {({ configuration, createDerivedIndexPattern }) => ( - - - {configuration ? ( - - ) : ( - - )} - - )} - + } )} - /> - - - + > + + + + + + + + + + + + + + {ADD_DATA_LABEL} + + + + + + + + ( + + {({ configuration, createDerivedIndexPattern }) => ( + + + {configuration ? ( + + ) : ( + + )} + + )} + + )} + /> + + + + diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout.tsx new file mode 100644 index 0000000000000..b063713fa2c97 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomoly_detection_flyout.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useCallback } from 'react'; +import { EuiButtonEmpty, EuiFlyout } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { FlyoutHome } from './flyout_home'; +import { JobSetupScreen } from './job_setup_screen'; +import { useInfraMLCapabilities } from '../../../../../../containers/ml/infra_ml_capabilities'; +import { MetricHostsModuleProvider } from '../../../../../../containers/ml/modules/metrics_hosts/module'; +import { MetricK8sModuleProvider } from '../../../../../../containers/ml/modules/metrics_k8s/module'; +import { useSourceViaHttp } from '../../../../../../containers/source/use_source_via_http'; +import { useActiveKibanaSpace } from '../../../../../../hooks/use_kibana_space'; + +export const AnomalyDetectionFlyout = () => { + const { hasInfraMLSetupCapabilities } = useInfraMLCapabilities(); + const [showFlyout, setShowFlyout] = useState(false); + const [screenName, setScreenName] = useState<'home' | 'setup'>('home'); + const [screenParams, setScreenParams] = useState(null); + const { source } = useSourceViaHttp({ + sourceId: 'default', + type: 'metrics', + }); + + const { space } = useActiveKibanaSpace(); + + const openFlyout = useCallback(() => { + setScreenName('home'); + setShowFlyout(true); + }, []); + + const openJobSetup = useCallback( + (jobType: 'hosts' | 'kubernetes') => { + setScreenName('setup'); + setScreenParams({ jobType }); + }, + [setScreenName] + ); + + const closeFlyout = useCallback(() => { + setShowFlyout(false); + }, []); + + if (source?.configuration.metricAlias == null || space == null) { + return null; + } + + return ( + <> + + + + {showFlyout && ( + + + + {screenName === 'home' && ( + + )} + {screenName === 'setup' && ( + + )} + + + + )} + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/flyout_home.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/flyout_home.tsx new file mode 100644 index 0000000000000..9cf898b684336 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/flyout_home.tsx @@ -0,0 +1,333 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useCallback, useEffect } from 'react'; +import { EuiFlyoutHeader, EuiTitle, EuiFlyoutBody, EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiText, EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut } from '@elastic/eui'; +import { EuiButton } from '@elastic/eui'; +import { EuiButtonEmpty } from '@elastic/eui'; +import moment from 'moment'; +import { useInfraMLCapabilitiesContext } from '../../../../../../containers/ml/infra_ml_capabilities'; +import { SubscriptionSplashContent } from './subscription_splash_content'; +import { + MissingResultsPrivilegesPrompt, + MissingSetupPrivilegesPrompt, +} from '../../../../../../components/logging/log_analysis_setup'; +import { useMetricHostsModuleContext } from '../../../../../../containers/ml/modules/metrics_hosts/module'; +import { useMetricK8sModuleContext } from '../../../../../../containers/ml/modules/metrics_k8s/module'; +import { LoadingPage } from '../../../../../../components/loading_page'; +import { useLinkProps } from '../../../../../../hooks/use_link_props'; + +interface Props { + hasSetupCapabilities: boolean; + goToSetup(type: 'hosts' | 'kubernetes'): void; +} + +export const FlyoutHome = (props: Props) => { + const [tab, setTab] = useState<'jobs' | 'anomalies'>('jobs'); + const { goToSetup } = props; + const { + fetchJobStatus: fetchHostJobStatus, + setupStatus: hostSetupStatus, + jobSummaries: hostJobSummaries, + } = useMetricHostsModuleContext(); + const { + fetchJobStatus: fetchK8sJobStatus, + setupStatus: k8sSetupStatus, + jobSummaries: k8sJobSummaries, + } = useMetricK8sModuleContext(); + const { + hasInfraMLCapabilites, + hasInfraMLReadCapabilities, + hasInfraMLSetupCapabilities, + } = useInfraMLCapabilitiesContext(); + + const createHosts = useCallback(() => { + goToSetup('hosts'); + }, [goToSetup]); + + const createK8s = useCallback(() => { + goToSetup('kubernetes'); + }, [goToSetup]); + + const goToJobs = useCallback(() => { + setTab('jobs'); + }, []); + + const jobIds = [ + ...(k8sJobSummaries || []).map((k) => k.id), + ...(hostJobSummaries || []).map((h) => h.id), + ]; + const anomaliesUrl = useLinkProps({ + app: 'ml', + pathname: `/explorer?_g=${createResultsUrl(jobIds)}`, + }); + + useEffect(() => { + if (hasInfraMLReadCapabilities) { + fetchHostJobStatus(); + fetchK8sJobStatus(); + } + }, [fetchK8sJobStatus, fetchHostJobStatus, hasInfraMLReadCapabilities]); + + if (!hasInfraMLCapabilites) { + return ; + } else if (!hasInfraMLReadCapabilities) { + return ; + } else if (hostSetupStatus.type === 'initializing' || k8sSetupStatus.type === 'initializing') { + return ( + + ); + } else if (!hasInfraMLSetupCapabilities) { + return ; + } else { + return ( + <> + + +

+ +

+
+
+ + + + + + + + + + + + {hostJobSummaries.length > 0 && ( + <> + 0} + hasK8sJobs={k8sJobSummaries.length > 0} + /> + + + )} + {tab === 'jobs' && ( + 0} + hasK8sJobs={k8sJobSummaries.length > 0} + hasSetupCapabilities={props.hasSetupCapabilities} + createHosts={createHosts} + createK8s={createK8s} + /> + )} + + + ); + } +}; + +interface CalloutProps { + hasHostJobs: boolean; + hasK8sJobs: boolean; +} +const JobsEnabledCallout = (props: CalloutProps) => { + let target = ''; + if (props.hasHostJobs && props.hasK8sJobs) { + target = `${i18n.translate('xpack.infra.ml.anomalyFlyout.create.hostTitle', { + defaultMessage: 'Hosts', + })} and ${i18n.translate('xpack.infra.ml.anomalyFlyout.create.k8sSuccessTitle', { + defaultMessage: 'Kubernetes', + })}`; + } else if (props.hasHostJobs) { + target = i18n.translate('xpack.infra.ml.anomalyFlyout.create.hostSuccessTitle', { + defaultMessage: 'Hosts', + }); + } else if (props.hasK8sJobs) { + target = i18n.translate('xpack.infra.ml.anomalyFlyout.create.k8sSuccessTitle', { + defaultMessage: 'Kubernetes', + }); + } + + const manageJobsLinkProps = useLinkProps({ + app: 'ml', + pathname: '/jobs', + }); + + return ( + <> + + } + iconType="check" + /> + + + + + + ); +}; + +interface CreateJobTab { + hasSetupCapabilities: boolean; + hasHostJobs: boolean; + hasK8sJobs: boolean; + createHosts(): void; + createK8s(): void; +} + +const CreateJobTab = (props: CreateJobTab) => { + return ( + <> +
+ +

+ +

+
+ +

+ +

+
+
+ + + + + } + // title="Hosts" + title={ + + } + description={ + + } + footer={ + <> + {props.hasHostJobs && ( + + + + )} + {!props.hasHostJobs && ( + + + + )} + + } + /> + + + } + title={ + + } + description={ + + } + footer={ + <> + {props.hasK8sJobs && ( + + + + )} + {!props.hasK8sJobs && ( + + + + )} + + } + /> + + + + ); +}; + +function createResultsUrl(jobIds: string[], mode = 'absolute') { + const idString = jobIds.map((j) => `'${j}'`).join(','); + let path = ''; + + const from = moment().subtract(4, 'weeks').toISOString(); + const to = moment().toISOString(); + + path += `(ml:(jobIds:!(${idString}))`; + path += `,refreshInterval:(display:Off,pause:!f,value:0),time:(from:'${from}'`; + path += `,to:'${to}'`; + if (mode === 'invalid') { + path += `,mode:invalid`; + } + path += "))&_a=(query:(query_string:(analyze_wildcard:!t,query:'*')))"; + + return path; +} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx new file mode 100644 index 0000000000000..730cd7b6e9ef5 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/job_setup_screen.tsx @@ -0,0 +1,277 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useCallback, useMemo, useEffect } from 'react'; +import { EuiForm, EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; +import { EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlyoutFooter } from '@elastic/eui'; +import { EuiButton } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import moment, { Moment } from 'moment'; +import { EuiComboBox } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { useSourceViaHttp } from '../../../../../../containers/source/use_source_via_http'; +import { useMetricK8sModuleContext } from '../../../../../../containers/ml/modules/metrics_k8s/module'; +import { useMetricHostsModuleContext } from '../../../../../../containers/ml/modules/metrics_hosts/module'; +import { FixedDatePicker } from '../../../../../../components/fixed_datepicker'; + +interface Props { + jobType: 'hosts' | 'kubernetes'; + closeFlyout(): void; + goHome(): void; +} + +export const JobSetupScreen = (props: Props) => { + const [now] = useState(() => moment()); + const { goHome } = props; + const [startDate, setStartDate] = useState(now.clone().subtract(4, 'weeks')); + const [partitionField, setPartitionField] = useState(null); + const h = useMetricHostsModuleContext(); + const k = useMetricK8sModuleContext(); + const { createDerivedIndexPattern } = useSourceViaHttp({ + sourceId: 'default', + type: 'metrics', + }); + + const indicies = h.sourceConfiguration.indices; + + const setupStatus = useMemo(() => { + if (props.jobType === 'kubernetes') { + return k.setupStatus; + } else { + return h.setupStatus; + } + }, [props.jobType, k.setupStatus, h.setupStatus]); + + const cleanUpAndSetUpModule = useMemo(() => { + if (props.jobType === 'kubernetes') { + return k.cleanUpAndSetUpModule; + } else { + return h.cleanUpAndSetUpModule; + } + }, [props.jobType, k.cleanUpAndSetUpModule, h.cleanUpAndSetUpModule]); + + const setUpModule = useMemo(() => { + if (props.jobType === 'kubernetes') { + return k.setUpModule; + } else { + return h.setUpModule; + } + }, [props.jobType, k.setUpModule, h.setUpModule]); + + const hasSummaries = useMemo(() => { + if (props.jobType === 'kubernetes') { + return k.jobSummaries.length > 0; + } else { + return h.jobSummaries.length > 0; + } + }, [props.jobType, k.jobSummaries, h.jobSummaries]); + + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ + createDerivedIndexPattern, + ]); + + const updateStart = useCallback((date: Moment) => { + setStartDate(date); + }, []); + + const createJobs = useCallback(() => { + if (hasSummaries) { + cleanUpAndSetUpModule( + indicies, + moment(startDate).toDate().getTime(), + undefined, + { type: 'includeAll' }, + partitionField ? partitionField[0] : undefined + ); + } else { + setUpModule( + indicies, + moment(startDate).toDate().getTime(), + undefined, + { type: 'includeAll' }, + partitionField ? partitionField[0] : undefined + ); + } + }, [cleanUpAndSetUpModule, setUpModule, hasSummaries, indicies, partitionField, startDate]); + + const onPartitionFieldChange = useCallback((value: Array<{ label: string }>) => { + setPartitionField(value.map((v) => v.label)); + }, []); + + useEffect(() => { + if (props.jobType === 'kubernetes') { + setPartitionField(['kubernetes.namespace']); + } + }, [props.jobType]); + + useEffect(() => { + if (setupStatus.type === 'succeeded') { + goHome(); + } + }, [setupStatus, goHome]); + + return ( + <> + + +

+ +

+
+
+ + {setupStatus.type === 'pending' ? ( + + + + + + + + + ) : setupStatus.type === 'failed' ? ( + <> + + + + + + + ) : ( + <> + +

+ +

+
+ + + + + + + } + description={ + + } + > + + } + > + + + + + + + + } + description={ + + } + > + + } + compressed + > + ({ label: p })) : undefined + } + options={derivedIndexPattern.fields + .filter((f) => f.aggregatable && f.type === 'string') + .map((f) => ({ label: f.name }))} + onChange={onPartitionFieldChange} + isClearable={true} + /> + + + + + )} +
+ + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx new file mode 100644 index 0000000000000..f07c37f5e7ea2 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/subscription_splash_content.tsx @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, + EuiText, + EuiButton, + EuiButtonEmpty, + EuiImage, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { LoadingPage } from '../../../../../../components/loading_page'; +import { useTrialStatus } from '../../../../../../hooks/use_trial_status'; +import { useKibana } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { euiStyled } from '../../../../../../../../observability/public'; +import { HttpStart } from '../../../../../../../../../../src/core/public'; + +export const SubscriptionSplashContent: React.FC = () => { + const { services } = useKibana<{ http: HttpStart }>(); + const { loadState, isTrialAvailable, checkTrialAvailability } = useTrialStatus(); + + useEffect(() => { + checkTrialAvailability(); + }, [checkTrialAvailability]); + + if (loadState === 'pending') { + return ( + + ); + } + + const canStartTrial = isTrialAvailable && loadState === 'resolved'; + + let title; + let description; + let cta; + + if (canStartTrial) { + title = ( + + ); + + description = ( + + ); + + cta = ( + + + + ); + } else { + title = ( + + ); + + description = ( + + ); + + cta = ( + + + + ); + } + + return ( + + + + + + +

{title}

+
+ + +

{description}

+
+ +
{cta}
+
+ + + +
+ + +

+ +

+
+ + + +
+
+
+
+ ); +}; + +const SubscriptionPage = euiStyled(EuiPage)` + height: 100% +`; + +const SubscriptionPageContent = euiStyled(EuiPageContent)` + max-width: 768px !important; +`; + +const SubscriptionPageFooter = euiStyled.div` + background: ${(props) => props.theme.eui.euiColorLightestShade}; + margin: 0 -${(props) => props.theme.eui.paddingSizes.l} -${(props) => + props.theme.eui.paddingSizes.l}; + padding: ${(props) => props.theme.eui.paddingSizes.l}; +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts new file mode 100644 index 0000000000000..f755057d0b76d --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts @@ -0,0 +1,318 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo, useState, useCallback, useEffect, useReducer } from 'react'; +import { + INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH, + Sort, + Pagination, + PaginationCursor, + getMetricsHostsAnomaliesRequestPayloadRT, + MetricsHostsAnomaly, + getMetricsHostsAnomaliesSuccessReponsePayloadRT, +} from '../../../../../common/http_api/infra_ml'; +import { useTrackedPromise } from '../../../../utils/use_tracked_promise'; +import { npStart } from '../../../../legacy_singletons'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; + +export type SortOptions = Sort; +export type PaginationOptions = Pick; +export type Page = number; +export type FetchNextPage = () => void; +export type FetchPreviousPage = () => void; +export type ChangeSortOptions = (sortOptions: Sort) => void; +export type ChangePaginationOptions = (paginationOptions: PaginationOptions) => void; +export type MetricsHostsAnomalies = MetricsHostsAnomaly[]; +interface PaginationCursors { + previousPageCursor: PaginationCursor; + nextPageCursor: PaginationCursor; +} + +interface ReducerState { + page: number; + lastReceivedCursors: PaginationCursors | undefined; + paginationCursor: Pagination['cursor'] | undefined; + hasNextPage: boolean; + paginationOptions: PaginationOptions; + sortOptions: Sort; + timeRange: { + start: number; + end: number; + }; + filteredDatasets?: string[]; +} + +type ReducerStateDefaults = Pick< + ReducerState, + 'page' | 'lastReceivedCursors' | 'paginationCursor' | 'hasNextPage' +>; + +type ReducerAction = + | { type: 'changePaginationOptions'; payload: { paginationOptions: PaginationOptions } } + | { type: 'changeSortOptions'; payload: { sortOptions: Sort } } + | { type: 'fetchNextPage' } + | { type: 'fetchPreviousPage' } + | { type: 'changeHasNextPage'; payload: { hasNextPage: boolean } } + | { type: 'changeLastReceivedCursors'; payload: { lastReceivedCursors: PaginationCursors } } + | { type: 'changeTimeRange'; payload: { timeRange: { start: number; end: number } } } + | { type: 'changeFilteredDatasets'; payload: { filteredDatasets?: string[] } }; + +const stateReducer = (state: ReducerState, action: ReducerAction): ReducerState => { + const resetPagination = { + page: 1, + paginationCursor: undefined, + }; + switch (action.type) { + case 'changePaginationOptions': + return { + ...state, + ...resetPagination, + ...action.payload, + }; + case 'changeSortOptions': + return { + ...state, + ...resetPagination, + ...action.payload, + }; + case 'changeHasNextPage': + return { + ...state, + ...action.payload, + }; + case 'changeLastReceivedCursors': + return { + ...state, + ...action.payload, + }; + case 'fetchNextPage': + return state.lastReceivedCursors + ? { + ...state, + page: state.page + 1, + paginationCursor: { searchAfter: state.lastReceivedCursors.nextPageCursor }, + } + : state; + case 'fetchPreviousPage': + return state.lastReceivedCursors + ? { + ...state, + page: state.page - 1, + paginationCursor: { searchBefore: state.lastReceivedCursors.previousPageCursor }, + } + : state; + case 'changeTimeRange': + return { + ...state, + ...resetPagination, + ...action.payload, + }; + case 'changeFilteredDatasets': + return { + ...state, + ...resetPagination, + ...action.payload, + }; + default: + return state; + } +}; + +const STATE_DEFAULTS: ReducerStateDefaults = { + // NOTE: This piece of state is purely for the client side, it could be extracted out of the hook. + page: 1, + // Cursor from the last request + lastReceivedCursors: undefined, + // Cursor to use for the next request. For the first request, and therefore not paging, this will be undefined. + paginationCursor: undefined, + hasNextPage: false, +}; + +export const useMetricsHostsAnomaliesResults = ({ + endTime, + startTime, + sourceId, + defaultSortOptions, + defaultPaginationOptions, + onGetMetricsHostsAnomaliesDatasetsError, + filteredDatasets, +}: { + endTime: number; + startTime: number; + sourceId: string; + defaultSortOptions: Sort; + defaultPaginationOptions: Pick; + onGetMetricsHostsAnomaliesDatasetsError?: (error: Error) => void; + filteredDatasets?: string[]; +}) => { + const initStateReducer = (stateDefaults: ReducerStateDefaults): ReducerState => { + return { + ...stateDefaults, + paginationOptions: defaultPaginationOptions, + sortOptions: defaultSortOptions, + filteredDatasets, + timeRange: { + start: startTime, + end: endTime, + }, + }; + }; + + const [reducerState, dispatch] = useReducer(stateReducer, STATE_DEFAULTS, initStateReducer); + + const [metricsHostsAnomalies, setMetricsHostsAnomalies] = useState([]); + + const [getMetricsHostsAnomaliesRequest, getMetricsHostsAnomalies] = useTrackedPromise( + { + cancelPreviousOn: 'creation', + createPromise: async () => { + const { + timeRange: { start: queryStartTime, end: queryEndTime }, + sortOptions, + paginationOptions, + paginationCursor, + } = reducerState; + return await callGetMetricHostsAnomaliesAPI( + sourceId, + queryStartTime, + queryEndTime, + sortOptions, + { + ...paginationOptions, + cursor: paginationCursor, + } + ); + }, + onResolve: ({ data: { anomalies, paginationCursors: requestCursors, hasMoreEntries } }) => { + const { paginationCursor } = reducerState; + if (requestCursors) { + dispatch({ + type: 'changeLastReceivedCursors', + payload: { lastReceivedCursors: requestCursors }, + }); + } + // Check if we have more "next" entries. "Page" covers the "previous" scenario, + // since we need to know the page we're on anyway. + if (!paginationCursor || (paginationCursor && 'searchAfter' in paginationCursor)) { + dispatch({ type: 'changeHasNextPage', payload: { hasNextPage: hasMoreEntries } }); + } else if (paginationCursor && 'searchBefore' in paginationCursor) { + // We've requested a previous page, therefore there is a next page. + dispatch({ type: 'changeHasNextPage', payload: { hasNextPage: true } }); + } + setMetricsHostsAnomalies(anomalies); + }, + }, + [ + sourceId, + dispatch, + reducerState.timeRange, + reducerState.sortOptions, + reducerState.paginationOptions, + reducerState.paginationCursor, + reducerState.filteredDatasets, + ] + ); + + const changeSortOptions = useCallback( + (nextSortOptions: Sort) => { + dispatch({ type: 'changeSortOptions', payload: { sortOptions: nextSortOptions } }); + }, + [dispatch] + ); + + const changePaginationOptions = useCallback( + (nextPaginationOptions: PaginationOptions) => { + dispatch({ + type: 'changePaginationOptions', + payload: { paginationOptions: nextPaginationOptions }, + }); + }, + [dispatch] + ); + + // Time range has changed + useEffect(() => { + dispatch({ + type: 'changeTimeRange', + payload: { timeRange: { start: startTime, end: endTime } }, + }); + }, [startTime, endTime]); + + // Selected datasets have changed + useEffect(() => { + dispatch({ + type: 'changeFilteredDatasets', + payload: { filteredDatasets }, + }); + }, [filteredDatasets]); + + useEffect(() => { + getMetricsHostsAnomalies(); + }, [getMetricsHostsAnomalies]); // TODO: FIgure out the deps here. + + const handleFetchNextPage = useCallback(() => { + if (reducerState.lastReceivedCursors) { + dispatch({ type: 'fetchNextPage' }); + } + }, [dispatch, reducerState]); + + const handleFetchPreviousPage = useCallback(() => { + if (reducerState.lastReceivedCursors) { + dispatch({ type: 'fetchPreviousPage' }); + } + }, [dispatch, reducerState]); + + const isLoadingMetricsHostsAnomalies = useMemo( + () => getMetricsHostsAnomaliesRequest.state === 'pending', + [getMetricsHostsAnomaliesRequest.state] + ); + + const hasFailedLoadingMetricsHostsAnomalies = useMemo( + () => getMetricsHostsAnomaliesRequest.state === 'rejected', + [getMetricsHostsAnomaliesRequest.state] + ); + + return { + metricsHostsAnomalies, + getMetricsHostsAnomalies, + isLoadingMetricsHostsAnomalies, + hasFailedLoadingMetricsHostsAnomalies, + changeSortOptions, + sortOptions: reducerState.sortOptions, + changePaginationOptions, + paginationOptions: reducerState.paginationOptions, + fetchPreviousPage: reducerState.page > 1 ? handleFetchPreviousPage : undefined, + fetchNextPage: reducerState.hasNextPage ? handleFetchNextPage : undefined, + page: reducerState.page, + }; +}; + +export const callGetMetricHostsAnomaliesAPI = async ( + sourceId: string, + startTime: number, + endTime: number, + sort: Sort, + pagination: Pagination +) => { + const response = await npStart.http.fetch(INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH, { + method: 'POST', + body: JSON.stringify( + getMetricsHostsAnomaliesRequestPayloadRT.encode({ + data: { + sourceId, + timeRange: { + startTime, + endTime, + }, + sort, + pagination, + }, + }) + ), + }); + + return decodeOrThrow(getMetricsHostsAnomaliesSuccessReponsePayloadRT)(response); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts new file mode 100644 index 0000000000000..4a7b78e1fdf92 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts @@ -0,0 +1,322 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo, useState, useCallback, useEffect, useReducer } from 'react'; +import { + Sort, + Pagination, + PaginationCursor, + INFA_ML_GET_METRICS_K8S_ANOMALIES_PATH, + getMetricsK8sAnomaliesSuccessReponsePayloadRT, + getMetricsK8sAnomaliesRequestPayloadRT, + MetricsK8sAnomaly, +} from '../../../../../common/http_api/infra_ml'; +import { useTrackedPromise } from '../../../../utils/use_tracked_promise'; +import { npStart } from '../../../../legacy_singletons'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; + +export type SortOptions = Sort; +export type PaginationOptions = Pick; +export type Page = number; +export type FetchNextPage = () => void; +export type FetchPreviousPage = () => void; +export type ChangeSortOptions = (sortOptions: Sort) => void; +export type ChangePaginationOptions = (paginationOptions: PaginationOptions) => void; +export type MetricsK8sAnomalies = MetricsK8sAnomaly[]; +interface PaginationCursors { + previousPageCursor: PaginationCursor; + nextPageCursor: PaginationCursor; +} + +interface ReducerState { + page: number; + lastReceivedCursors: PaginationCursors | undefined; + paginationCursor: Pagination['cursor'] | undefined; + hasNextPage: boolean; + paginationOptions: PaginationOptions; + sortOptions: Sort; + timeRange: { + start: number; + end: number; + }; + filteredDatasets?: string[]; +} + +type ReducerStateDefaults = Pick< + ReducerState, + 'page' | 'lastReceivedCursors' | 'paginationCursor' | 'hasNextPage' +>; + +type ReducerAction = + | { type: 'changePaginationOptions'; payload: { paginationOptions: PaginationOptions } } + | { type: 'changeSortOptions'; payload: { sortOptions: Sort } } + | { type: 'fetchNextPage' } + | { type: 'fetchPreviousPage' } + | { type: 'changeHasNextPage'; payload: { hasNextPage: boolean } } + | { type: 'changeLastReceivedCursors'; payload: { lastReceivedCursors: PaginationCursors } } + | { type: 'changeTimeRange'; payload: { timeRange: { start: number; end: number } } } + | { type: 'changeFilteredDatasets'; payload: { filteredDatasets?: string[] } }; + +const stateReducer = (state: ReducerState, action: ReducerAction): ReducerState => { + const resetPagination = { + page: 1, + paginationCursor: undefined, + }; + switch (action.type) { + case 'changePaginationOptions': + return { + ...state, + ...resetPagination, + ...action.payload, + }; + case 'changeSortOptions': + return { + ...state, + ...resetPagination, + ...action.payload, + }; + case 'changeHasNextPage': + return { + ...state, + ...action.payload, + }; + case 'changeLastReceivedCursors': + return { + ...state, + ...action.payload, + }; + case 'fetchNextPage': + return state.lastReceivedCursors + ? { + ...state, + page: state.page + 1, + paginationCursor: { searchAfter: state.lastReceivedCursors.nextPageCursor }, + } + : state; + case 'fetchPreviousPage': + return state.lastReceivedCursors + ? { + ...state, + page: state.page - 1, + paginationCursor: { searchBefore: state.lastReceivedCursors.previousPageCursor }, + } + : state; + case 'changeTimeRange': + return { + ...state, + ...resetPagination, + ...action.payload, + }; + case 'changeFilteredDatasets': + return { + ...state, + ...resetPagination, + ...action.payload, + }; + default: + return state; + } +}; + +const STATE_DEFAULTS: ReducerStateDefaults = { + // NOTE: This piece of state is purely for the client side, it could be extracted out of the hook. + page: 1, + // Cursor from the last request + lastReceivedCursors: undefined, + // Cursor to use for the next request. For the first request, and therefore not paging, this will be undefined. + paginationCursor: undefined, + hasNextPage: false, +}; + +export const useMetricsK8sAnomaliesResults = ({ + endTime, + startTime, + sourceId, + defaultSortOptions, + defaultPaginationOptions, + onGetMetricsHostsAnomaliesDatasetsError, + filteredDatasets, +}: { + endTime: number; + startTime: number; + sourceId: string; + defaultSortOptions: Sort; + defaultPaginationOptions: Pick; + onGetMetricsHostsAnomaliesDatasetsError?: (error: Error) => void; + filteredDatasets?: string[]; +}) => { + const initStateReducer = (stateDefaults: ReducerStateDefaults): ReducerState => { + return { + ...stateDefaults, + paginationOptions: defaultPaginationOptions, + sortOptions: defaultSortOptions, + filteredDatasets, + timeRange: { + start: startTime, + end: endTime, + }, + }; + }; + + const [reducerState, dispatch] = useReducer(stateReducer, STATE_DEFAULTS, initStateReducer); + + const [metricsK8sAnomalies, setMetricsK8sAnomalies] = useState([]); + + const [getMetricsK8sAnomaliesRequest, getMetricsK8sAnomalies] = useTrackedPromise( + { + cancelPreviousOn: 'creation', + createPromise: async () => { + const { + timeRange: { start: queryStartTime, end: queryEndTime }, + sortOptions, + paginationOptions, + paginationCursor, + filteredDatasets: queryFilteredDatasets, + } = reducerState; + return await callGetMetricsK8sAnomaliesAPI( + sourceId, + queryStartTime, + queryEndTime, + sortOptions, + { + ...paginationOptions, + cursor: paginationCursor, + }, + queryFilteredDatasets + ); + }, + onResolve: ({ data: { anomalies, paginationCursors: requestCursors, hasMoreEntries } }) => { + const { paginationCursor } = reducerState; + if (requestCursors) { + dispatch({ + type: 'changeLastReceivedCursors', + payload: { lastReceivedCursors: requestCursors }, + }); + } + // Check if we have more "next" entries. "Page" covers the "previous" scenario, + // since we need to know the page we're on anyway. + if (!paginationCursor || (paginationCursor && 'searchAfter' in paginationCursor)) { + dispatch({ type: 'changeHasNextPage', payload: { hasNextPage: hasMoreEntries } }); + } else if (paginationCursor && 'searchBefore' in paginationCursor) { + // We've requested a previous page, therefore there is a next page. + dispatch({ type: 'changeHasNextPage', payload: { hasNextPage: true } }); + } + setMetricsK8sAnomalies(anomalies); + }, + }, + [ + sourceId, + dispatch, + reducerState.timeRange, + reducerState.sortOptions, + reducerState.paginationOptions, + reducerState.paginationCursor, + reducerState.filteredDatasets, + ] + ); + + const changeSortOptions = useCallback( + (nextSortOptions: Sort) => { + dispatch({ type: 'changeSortOptions', payload: { sortOptions: nextSortOptions } }); + }, + [dispatch] + ); + + const changePaginationOptions = useCallback( + (nextPaginationOptions: PaginationOptions) => { + dispatch({ + type: 'changePaginationOptions', + payload: { paginationOptions: nextPaginationOptions }, + }); + }, + [dispatch] + ); + + // Time range has changed + useEffect(() => { + dispatch({ + type: 'changeTimeRange', + payload: { timeRange: { start: startTime, end: endTime } }, + }); + }, [startTime, endTime]); + + // Selected datasets have changed + useEffect(() => { + dispatch({ + type: 'changeFilteredDatasets', + payload: { filteredDatasets }, + }); + }, [filteredDatasets]); + + useEffect(() => { + getMetricsK8sAnomalies(); + }, [getMetricsK8sAnomalies]); + + const handleFetchNextPage = useCallback(() => { + if (reducerState.lastReceivedCursors) { + dispatch({ type: 'fetchNextPage' }); + } + }, [dispatch, reducerState]); + + const handleFetchPreviousPage = useCallback(() => { + if (reducerState.lastReceivedCursors) { + dispatch({ type: 'fetchPreviousPage' }); + } + }, [dispatch, reducerState]); + + const isLoadingMetricsK8sAnomalies = useMemo( + () => getMetricsK8sAnomaliesRequest.state === 'pending', + [getMetricsK8sAnomaliesRequest.state] + ); + + const hasFailedLoadingMetricsK8sAnomalies = useMemo( + () => getMetricsK8sAnomaliesRequest.state === 'rejected', + [getMetricsK8sAnomaliesRequest.state] + ); + + return { + metricsK8sAnomalies, + getMetricsK8sAnomalies, + isLoadingMetricsK8sAnomalies, + hasFailedLoadingMetricsK8sAnomalies, + changeSortOptions, + sortOptions: reducerState.sortOptions, + changePaginationOptions, + paginationOptions: reducerState.paginationOptions, + fetchPreviousPage: reducerState.page > 1 ? handleFetchPreviousPage : undefined, + fetchNextPage: reducerState.hasNextPage ? handleFetchNextPage : undefined, + page: reducerState.page, + }; +}; + +export const callGetMetricsK8sAnomaliesAPI = async ( + sourceId: string, + startTime: number, + endTime: number, + sort: Sort, + pagination: Pagination, + datasets?: string[] +) => { + const response = await npStart.http.fetch(INFA_ML_GET_METRICS_K8S_ANOMALIES_PATH, { + method: 'POST', + body: JSON.stringify( + getMetricsK8sAnomaliesRequestPayloadRT.encode({ + data: { + sourceId, + timeRange: { + startTime, + endTime, + }, + sort, + pagination, + datasets, + }, + }) + ), + }); + + return decodeOrThrow(getMetricsK8sAnomaliesSuccessReponsePayloadRT)(response); +}; diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index a72e40e25b479..206fffdd2e188 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -21,6 +21,8 @@ import { initGetLogEntryAnomaliesRoute, initGetLogEntryAnomaliesDatasetsRoute, } from './routes/log_analysis'; +import { initGetK8sAnomaliesRoute } from './routes/infra_ml'; +import { initGetHostsAnomaliesRoute } from './routes/infra_ml'; import { initMetricExplorerRoute } from './routes/metrics_explorer'; import { initMetadataRoute } from './routes/metadata'; import { initSnapshotRoute } from './routes/snapshot'; @@ -56,6 +58,8 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initGetLogEntryRateRoute(libs); initGetLogEntryAnomaliesRoute(libs); initGetLogEntryAnomaliesDatasetsRoute(libs); + initGetK8sAnomaliesRoute(libs); + initGetHostsAnomaliesRoute(libs); initSnapshotRoute(libs); initNodeDetailsRoute(libs); initSourceRoute(libs); diff --git a/x-pack/plugins/infra/server/lib/infra_ml/common.ts b/x-pack/plugins/infra/server/lib/infra_ml/common.ts new file mode 100644 index 0000000000000..4d2be94c7cd62 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/infra_ml/common.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import type { MlAnomalyDetectors, MlSystem } from '../../types'; +import { NoLogAnalysisMlJobError } from './errors'; + +import { + CompositeDatasetKey, + createLogEntryDatasetsQuery, + LogEntryDatasetBucket, + logEntryDatasetsResponseRT, +} from './queries/log_entry_data_sets'; +import { decodeOrThrow } from '../../../common/runtime_types'; +import { startTracingSpan, TracingSpan } from '../../../common/performance_tracing'; + +export async function fetchMlJob(mlAnomalyDetectors: MlAnomalyDetectors, jobId: string) { + const finalizeMlGetJobSpan = startTracingSpan('Fetch ml job from ES'); + const { + jobs: [mlJob], + } = await mlAnomalyDetectors.jobs(jobId); + + const mlGetJobSpan = finalizeMlGetJobSpan(); + + if (mlJob == null) { + throw new NoLogAnalysisMlJobError(`Failed to find ml job ${jobId}.`); + } + + return { + mlJob, + timing: { + spans: [mlGetJobSpan], + }, + }; +} + +const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000; + +// Finds datasets related to ML job ids +export async function getLogEntryDatasets( + mlSystem: MlSystem, + startTime: number, + endTime: number, + jobIds: string[] +) { + const finalizeLogEntryDatasetsSpan = startTracingSpan('get data sets'); + + let logEntryDatasetBuckets: LogEntryDatasetBucket[] = []; + let afterLatestBatchKey: CompositeDatasetKey | undefined; + let esSearchSpans: TracingSpan[] = []; + + while (true) { + const finalizeEsSearchSpan = startTracingSpan('fetch log entry dataset batch from ES'); + + const logEntryDatasetsResponse = decodeOrThrow(logEntryDatasetsResponseRT)( + await mlSystem.mlAnomalySearch( + createLogEntryDatasetsQuery( + jobIds, + startTime, + endTime, + COMPOSITE_AGGREGATION_BATCH_SIZE, + afterLatestBatchKey + ) + ) + ); + + const { after_key: afterKey, buckets: latestBatchBuckets = [] } = + logEntryDatasetsResponse.aggregations?.dataset_buckets ?? {}; + + logEntryDatasetBuckets = [...logEntryDatasetBuckets, ...latestBatchBuckets]; + afterLatestBatchKey = afterKey; + esSearchSpans = [...esSearchSpans, finalizeEsSearchSpan()]; + + if (latestBatchBuckets.length < COMPOSITE_AGGREGATION_BATCH_SIZE) { + break; + } + } + + const logEntryDatasetsSpan = finalizeLogEntryDatasetsSpan(); + + return { + data: logEntryDatasetBuckets.map((logEntryDatasetBucket) => logEntryDatasetBucket.key.dataset), + timing: { + spans: [logEntryDatasetsSpan, ...esSearchSpans], + }, + }; +} diff --git a/x-pack/plugins/infra/server/lib/infra_ml/errors.ts b/x-pack/plugins/infra/server/lib/infra_ml/errors.ts new file mode 100644 index 0000000000000..ad46ebf710266 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/infra_ml/errors.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable max-classes-per-file */ + +import { + UnknownMLCapabilitiesError, + InsufficientMLCapabilities, + MLPrivilegesUninitialized, +} from '../../../../ml/server'; + +export class NoLogAnalysisMlJobError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export class InsufficientLogAnalysisMlJobConfigurationError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export class UnknownCategoryError extends Error { + constructor(categoryId: number) { + super(`Unknown ml category ${categoryId}`); + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export class InsufficientAnomalyMlJobsConfigured extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export const isMlPrivilegesError = (error: any) => { + return ( + error instanceof UnknownMLCapabilitiesError || + error instanceof InsufficientMLCapabilities || + error instanceof MLPrivilegesUninitialized + ); +}; diff --git a/x-pack/plugins/infra/server/lib/infra_ml/index.ts b/x-pack/plugins/infra/server/lib/infra_ml/index.ts new file mode 100644 index 0000000000000..536f0a44d5890 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/infra_ml/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './errors'; +export * from './metrics_hosts_anomalies'; +export * from './metrics_k8s_anomalies'; diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts new file mode 100644 index 0000000000000..e0afa458aac88 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts @@ -0,0 +1,289 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RequestHandlerContext } from 'src/core/server'; +import { InfraRequestHandlerContext } from '../../types'; +import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing'; +import { fetchMlJob, getLogEntryDatasets } from './common'; +import { getJobId, metricsHostsJobTypes } from '../../../common/infra_ml'; +import { Sort, Pagination } from '../../../common/http_api/infra_ml'; +import type { MlSystem, MlAnomalyDetectors } from '../../types'; +import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors'; +import { decodeOrThrow } from '../../../common/runtime_types'; +import { + metricsHostsAnomaliesResponseRT, + createMetricsHostsAnomaliesQuery, +} from './queries/metrics_hosts_anomalies'; + +interface MappedAnomalyHit { + id: string; + anomalyScore: number; + dataset: string; + typical: number; + actual: number; + jobId: string; + startTime: number; + duration: number; + hostName: string[]; + categoryId?: string; +} + +async function getCompatibleAnomaliesJobIds( + spaceId: string, + sourceId: string, + mlAnomalyDetectors: MlAnomalyDetectors +) { + const metricsHostsJobIds = metricsHostsJobTypes.map((jt) => getJobId(spaceId, sourceId, jt)); + + const jobIds: string[] = []; + let jobSpans: TracingSpan[] = []; + + try { + await Promise.all( + metricsHostsJobIds.map((id) => { + return (async () => { + const { + timing: { spans }, + } = await fetchMlJob(mlAnomalyDetectors, id); + jobIds.push(id); + jobSpans = [...jobSpans, ...spans]; + })(); + }) + ); + } catch (e) { + if (isMlPrivilegesError(e)) { + throw e; + } + // An error is also thrown when no jobs are found + } + + return { + jobIds, + timing: { spans: jobSpans }, + }; +} + +export async function getMetricsHostsAnomalies( + context: RequestHandlerContext & { infra: Required }, + sourceId: string, + startTime: number, + endTime: number, + sort: Sort, + pagination: Pagination +) { + const finalizeMetricsHostsAnomaliesSpan = startTracingSpan('get metrics hosts entry anomalies'); + + const { + jobIds, + timing: { spans: jobSpans }, + } = await getCompatibleAnomaliesJobIds( + context.infra.spaceId, + sourceId, + context.infra.mlAnomalyDetectors + ); + + if (jobIds.length === 0) { + throw new InsufficientAnomalyMlJobsConfigured( + 'Metrics Hosts ML jobs need to be configured to search anomalies' + ); + } + + try { + const { + anomalies, + paginationCursors, + hasMoreEntries, + timing: { spans: fetchLogEntryAnomaliesSpans }, + } = await fetchMetricsHostsAnomalies( + context.infra.mlSystem, + jobIds, + startTime, + endTime, + sort, + pagination + ); + + const data = anomalies.map((anomaly) => { + const { jobId } = anomaly; + + return parseAnomalyResult(anomaly, jobId); + }); + + const metricsHostsAnomaliesSpan = finalizeMetricsHostsAnomaliesSpan(); + + return { + data, + paginationCursors, + hasMoreEntries, + timing: { + spans: [metricsHostsAnomaliesSpan, ...jobSpans, ...fetchLogEntryAnomaliesSpans], + }, + }; + } catch (e) { + throw new Error(e); + } +} + +const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => { + const { + id, + anomalyScore, + dataset, + typical, + actual, + duration, + hostName, + startTime: anomalyStartTime, + } = anomaly; + + return { + id, + anomalyScore, + dataset, + typical, + actual, + duration, + hostName, + startTime: anomalyStartTime, + type: 'metrics_hosts' as const, + jobId, + }; +}; + +async function fetchMetricsHostsAnomalies( + mlSystem: MlSystem, + jobIds: string[], + startTime: number, + endTime: number, + sort: Sort, + pagination: Pagination +) { + // We'll request 1 extra entry on top of our pageSize to determine if there are + // more entries to be fetched. This avoids scenarios where the client side can't + // determine if entries.length === pageSize actually means there are more entries / next page + // or not. + const expandedPagination = { ...pagination, pageSize: pagination.pageSize + 1 }; + + const finalizeFetchLogEntryAnomaliesSpan = startTracingSpan('fetch metrics hosts anomalies'); + + // console.log( + // 'data', + // JSON.stringify( + // await mlSystem.mlAnomalySearch( + // createMetricsHostsAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination) + // ), + // null, + // 2 + // ) + // ); + const results = decodeOrThrow(metricsHostsAnomaliesResponseRT)( + await mlSystem.mlAnomalySearch( + createMetricsHostsAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination) + ) + ); + + const { + hits: { hits }, + } = results; + const hasMoreEntries = hits.length > pagination.pageSize; + + // An extra entry was found and hasMoreEntries has been determined, the extra entry can be removed. + if (hasMoreEntries) { + hits.pop(); + } + + // To "search_before" the sort order will have been reversed for ES. + // The results are now reversed back, to match the requested sort. + if (pagination.cursor && 'searchBefore' in pagination.cursor) { + hits.reverse(); + } + + const paginationCursors = + hits.length > 0 + ? { + previousPageCursor: hits[0].sort, + nextPageCursor: hits[hits.length - 1].sort, + } + : undefined; + + const anomalies = hits.map((result) => { + const { + // eslint-disable-next-line @typescript-eslint/naming-convention + job_id, + record_score: anomalyScore, + typical, + actual, + bucket_span: duration, + timestamp: anomalyStartTime, + by_field_value: categoryId, + } = result._source; + + return { + id: result._id, + anomalyScore, + dataset: '', + typical: typical[0], + actual: actual[0], + jobId: job_id, + hostName: result._source['host.name'], + startTime: anomalyStartTime, + duration: duration * 1000, + categoryId, + }; + }); + + const fetchLogEntryAnomaliesSpan = finalizeFetchLogEntryAnomaliesSpan(); + + return { + anomalies, + paginationCursors, + hasMoreEntries, + timing: { + spans: [fetchLogEntryAnomaliesSpan], + }, + }; +} + +// TODO: FIgure out why we need datasets +export async function getMetricsHostsAnomaliesDatasets( + context: { + infra: { + mlSystem: MlSystem; + mlAnomalyDetectors: MlAnomalyDetectors; + spaceId: string; + }; + }, + sourceId: string, + startTime: number, + endTime: number +) { + const { + jobIds, + timing: { spans: jobSpans }, + } = await getCompatibleAnomaliesJobIds( + context.infra.spaceId, + sourceId, + context.infra.mlAnomalyDetectors + ); + + if (jobIds.length === 0) { + throw new InsufficientAnomalyMlJobsConfigured( + 'Log rate or categorisation ML jobs need to be configured to search for anomaly datasets' + ); + } + + const { + data: datasets, + timing: { spans: datasetsSpans }, + } = await getLogEntryDatasets(context.infra.mlSystem, startTime, endTime, jobIds); + + return { + datasets, + timing: { + spans: [...jobSpans, ...datasetsSpans], + }, + }; +} diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts new file mode 100644 index 0000000000000..29507900e1847 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts @@ -0,0 +1,272 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RequestHandlerContext } from 'src/core/server'; +import { InfraRequestHandlerContext } from '../../types'; +import { TracingSpan, startTracingSpan } from '../../../common/performance_tracing'; +import { fetchMlJob, getLogEntryDatasets } from './common'; +import { getJobId, metricsK8SJobTypes } from '../../../common/infra_ml'; +import { Sort, Pagination } from '../../../common/http_api/infra_ml'; +import type { MlSystem, MlAnomalyDetectors } from '../../types'; +import { InsufficientAnomalyMlJobsConfigured, isMlPrivilegesError } from './errors'; +import { decodeOrThrow } from '../../../common/runtime_types'; +import { + metricsK8sAnomaliesResponseRT, + createMetricsK8sAnomaliesQuery, +} from './queries/metrics_k8s_anomalies'; + +interface MappedAnomalyHit { + id: string; + anomalyScore: number; + // dataset: string; + typical: number; + actual: number; + jobId: string; + startTime: number; + duration: number; + categoryId?: string; +} + +async function getCompatibleAnomaliesJobIds( + spaceId: string, + sourceId: string, + mlAnomalyDetectors: MlAnomalyDetectors +) { + const metricsK8sJobIds = metricsK8SJobTypes.map((jt) => getJobId(spaceId, sourceId, jt)); + + const jobIds: string[] = []; + let jobSpans: TracingSpan[] = []; + + try { + await Promise.all( + metricsK8sJobIds.map((id) => { + return (async () => { + const { + timing: { spans }, + } = await fetchMlJob(mlAnomalyDetectors, id); + jobIds.push(id); + jobSpans = [...jobSpans, ...spans]; + })(); + }) + ); + } catch (e) { + if (isMlPrivilegesError(e)) { + throw e; + } + // An error is also thrown when no jobs are found + } + + return { + jobIds, + timing: { spans: jobSpans }, + }; +} + +export async function getMetricK8sAnomalies( + context: RequestHandlerContext & { infra: Required }, + sourceId: string, + startTime: number, + endTime: number, + sort: Sort, + pagination: Pagination +) { + const finalizeMetricsK8sAnomaliesSpan = startTracingSpan('get metrics k8s entry anomalies'); + + const { + jobIds, + timing: { spans: jobSpans }, + } = await getCompatibleAnomaliesJobIds( + context.infra.spaceId, + sourceId, + context.infra.mlAnomalyDetectors + ); + + if (jobIds.length === 0) { + throw new InsufficientAnomalyMlJobsConfigured( + 'Log rate or categorisation ML jobs need to be configured to search anomalies' + ); + } + + const { + anomalies, + paginationCursors, + hasMoreEntries, + timing: { spans: fetchLogEntryAnomaliesSpans }, + } = await fetchMetricK8sAnomalies( + context.infra.mlSystem, + jobIds, + startTime, + endTime, + sort, + pagination + ); + + const data = anomalies.map((anomaly) => { + const { jobId } = anomaly; + + return parseAnomalyResult(anomaly, jobId); + }); + + const metricsK8sAnomaliesSpan = finalizeMetricsK8sAnomaliesSpan(); + + return { + data, + paginationCursors, + hasMoreEntries, + timing: { + spans: [metricsK8sAnomaliesSpan, ...jobSpans, ...fetchLogEntryAnomaliesSpans], + }, + }; +} + +const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => { + const { + id, + anomalyScore, + // dataset, + typical, + actual, + duration, + startTime: anomalyStartTime, + } = anomaly; + + return { + id, + anomalyScore, + // dataset, + typical, + actual, + duration, + startTime: anomalyStartTime, + type: 'metrics_k8s' as const, + jobId, + }; +}; + +async function fetchMetricK8sAnomalies( + mlSystem: MlSystem, + jobIds: string[], + startTime: number, + endTime: number, + sort: Sort, + pagination: Pagination +) { + // We'll request 1 extra entry on top of our pageSize to determine if there are + // more entries to be fetched. This avoids scenarios where the client side can't + // determine if entries.length === pageSize actually means there are more entries / next page + // or not. + const expandedPagination = { ...pagination, pageSize: pagination.pageSize + 1 }; + + const finalizeFetchLogEntryAnomaliesSpan = startTracingSpan('fetch metrics k8s anomalies'); + + const results = decodeOrThrow(metricsK8sAnomaliesResponseRT)( + await mlSystem.mlAnomalySearch( + createMetricsK8sAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination) + ) + ); + + const { + hits: { hits }, + } = results; + const hasMoreEntries = hits.length > pagination.pageSize; + + // An extra entry was found and hasMoreEntries has been determined, the extra entry can be removed. + if (hasMoreEntries) { + hits.pop(); + } + + // To "search_before" the sort order will have been reversed for ES. + // The results are now reversed back, to match the requested sort. + if (pagination.cursor && 'searchBefore' in pagination.cursor) { + hits.reverse(); + } + + const paginationCursors = + hits.length > 0 + ? { + previousPageCursor: hits[0].sort, + nextPageCursor: hits[hits.length - 1].sort, + } + : undefined; + + const anomalies = hits.map((result) => { + const { + // eslint-disable-next-line @typescript-eslint/naming-convention + job_id, + record_score: anomalyScore, + typical, + actual, + // partition_field_value: dataset, + bucket_span: duration, + timestamp: anomalyStartTime, + by_field_value: categoryId, + } = result._source; + + return { + id: result._id, + anomalyScore, + // dataset, + typical: typical[0], + actual: actual[0], + jobId: job_id, + startTime: anomalyStartTime, + duration: duration * 1000, + categoryId, + }; + }); + + const fetchLogEntryAnomaliesSpan = finalizeFetchLogEntryAnomaliesSpan(); + + return { + anomalies, + paginationCursors, + hasMoreEntries, + timing: { + spans: [fetchLogEntryAnomaliesSpan], + }, + }; +} + +// TODO: FIgure out why we need datasets +export async function getMetricK8sAnomaliesDatasets( + context: { + infra: { + mlSystem: MlSystem; + mlAnomalyDetectors: MlAnomalyDetectors; + spaceId: string; + }; + }, + sourceId: string, + startTime: number, + endTime: number +) { + const { + jobIds, + timing: { spans: jobSpans }, + } = await getCompatibleAnomaliesJobIds( + context.infra.spaceId, + sourceId, + context.infra.mlAnomalyDetectors + ); + + if (jobIds.length === 0) { + throw new InsufficientAnomalyMlJobsConfigured( + 'Log rate or categorisation ML jobs need to be configured to search for anomaly datasets' + ); + } + + const { + data: datasets, + timing: { spans: datasetsSpans }, + } = await getLogEntryDatasets(context.infra.mlSystem, startTime, endTime, jobIds); + + return { + datasets, + timing: { + spans: [...jobSpans, ...datasetsSpans], + }, + }; +} diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts new file mode 100644 index 0000000000000..63e39ef022392 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/common.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +export const defaultRequestParameters = { + allowNoIndices: true, + ignoreUnavailable: true, + trackScores: false, + trackTotalHits: false, +}; + +export const createJobIdFilters = (jobId: string) => [ + { + term: { + job_id: { + value: jobId, + }, + }, + }, +]; + +export const createJobIdsFilters = (jobIds: string[]) => [ + { + terms: { + job_id: jobIds, + }, + }, +]; + +export const createTimeRangeFilters = (startTime: number, endTime: number) => [ + { + range: { + timestamp: { + gte: startTime, + lte: endTime, + }, + }, + }, +]; + +export const createResultTypeFilters = (resultTypes: Array<'model_plot' | 'record'>) => [ + { + terms: { + result_type: resultTypes, + }, + }, +]; + +export const createCategoryIdFilters = (categoryIds: number[]) => [ + { + terms: { + category_id: categoryIds, + }, + }, +]; + +export const createDatasetsFilters = (datasets?: string[]) => + datasets && datasets.length > 0 + ? [ + { + terms: { + partition_field_value: datasets, + }, + }, + ] + : []; diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/index.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/index.ts new file mode 100644 index 0000000000000..5a42011e1cea1 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export * from './metrics_k8s_anomalies'; +export * from './metrics_hosts_anomalies'; diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/log_entry_data_sets.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/log_entry_data_sets.ts new file mode 100644 index 0000000000000..53971a91d86b1 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/log_entry_data_sets.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; +import { + createJobIdsFilters, + createResultTypeFilters, + createTimeRangeFilters, + defaultRequestParameters, +} from './common'; + +export const createLogEntryDatasetsQuery = ( + jobIds: string[], + startTime: number, + endTime: number, + size: number, + afterKey?: CompositeDatasetKey +) => ({ + ...defaultRequestParameters, + body: { + query: { + bool: { + filter: [ + ...createJobIdsFilters(jobIds), + ...createTimeRangeFilters(startTime, endTime), + ...createResultTypeFilters(['model_plot']), + ], + }, + }, + aggs: { + dataset_buckets: { + composite: { + after: afterKey, + size, + sources: [ + { + dataset: { + terms: { + field: 'partition_field_value', + order: 'asc', + }, + }, + }, + ], + }, + }, + }, + }, + size: 0, +}); + +const compositeDatasetKeyRT = rt.type({ + dataset: rt.string, +}); + +export type CompositeDatasetKey = rt.TypeOf; + +const logEntryDatasetBucketRT = rt.type({ + key: compositeDatasetKeyRT, +}); + +export type LogEntryDatasetBucket = rt.TypeOf; + +export const logEntryDatasetsResponseRT = rt.intersection([ + commonSearchSuccessResponseFieldsRT, + rt.partial({ + aggregations: rt.type({ + dataset_buckets: rt.intersection([ + rt.type({ + buckets: rt.array(logEntryDatasetBucketRT), + }), + rt.partial({ + after_key: compositeDatasetKeyRT, + }), + ]), + }), + }), +]); + +export type LogEntryDatasetsResponse = rt.TypeOf; diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts new file mode 100644 index 0000000000000..b61119b60bc18 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; +import { + createJobIdsFilters, + createTimeRangeFilters, + createResultTypeFilters, + defaultRequestParameters, +} from './common'; +import { Sort, Pagination } from '../../../../common/http_api/infra_ml'; + +// TODO: Reassess validity of this against ML docs +const TIEBREAKER_FIELD = '_doc'; + +const sortToMlFieldMap = { + dataset: 'partition_field_value', + anomalyScore: 'record_score', + startTime: 'timestamp', +}; + +export const createMetricsHostsAnomaliesQuery = ( + jobIds: string[], + startTime: number, + endTime: number, + sort: Sort, + pagination: Pagination +) => { + const { field } = sort; + const { pageSize } = pagination; + + const filters = [ + ...createJobIdsFilters(jobIds), + ...createTimeRangeFilters(startTime, endTime), + ...createResultTypeFilters(['record']), + ]; + + const sourceFields = [ + 'job_id', + 'record_score', + 'typical', + 'actual', + 'partition_field_value', + 'timestamp', + 'bucket_span', + 'by_field_value', + 'host.name', + 'influencers.influencer_field_name', + 'influencers.influencer_field_values', + ]; + + const { querySortDirection, queryCursor } = parsePaginationCursor(sort, pagination); + + const sortOptions = [ + { [sortToMlFieldMap[field]]: querySortDirection }, + { [TIEBREAKER_FIELD]: querySortDirection }, // Tiebreaker + ]; + + const resultsQuery = { + ...defaultRequestParameters, + body: { + query: { + bool: { + filter: filters, + }, + }, + search_after: queryCursor, + sort: sortOptions, + size: pageSize, + _source: sourceFields, + }, + }; + + return resultsQuery; +}; + +export const metricsHostsAnomalyHitRT = rt.type({ + _id: rt.string, + _source: rt.intersection([ + rt.type({ + job_id: rt.string, + record_score: rt.number, + typical: rt.array(rt.number), + actual: rt.array(rt.number), + 'host.name': rt.array(rt.string), + bucket_span: rt.number, + timestamp: rt.number, + }), + rt.partial({ + by_field_value: rt.string, + }), + ]), + sort: rt.tuple([rt.union([rt.string, rt.number]), rt.union([rt.string, rt.number])]), +}); + +export type MetricsHostsAnomalyHit = rt.TypeOf; + +export const metricsHostsAnomaliesResponseRT = rt.intersection([ + commonSearchSuccessResponseFieldsRT, + rt.type({ + hits: rt.type({ + hits: rt.array(metricsHostsAnomalyHitRT), + }), + }), +]); + +export type MetricsHostsAnomaliesResponseRT = rt.TypeOf; + +const parsePaginationCursor = (sort: Sort, pagination: Pagination) => { + const { cursor } = pagination; + const { direction } = sort; + + if (!cursor) { + return { querySortDirection: direction, queryCursor: undefined }; + } + + // We will always use ES's search_after to paginate, to mimic "search_before" behaviour we + // need to reverse the user's chosen search direction for the ES query. + if ('searchBefore' in cursor) { + return { + querySortDirection: direction === 'desc' ? 'asc' : 'desc', + queryCursor: cursor.searchBefore, + }; + } else { + return { querySortDirection: direction, queryCursor: cursor.searchAfter }; + } +}; diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts new file mode 100644 index 0000000000000..84ed8b064c5ca --- /dev/null +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; +import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; +import { + createJobIdsFilters, + createTimeRangeFilters, + createResultTypeFilters, + defaultRequestParameters, +} from './common'; +import { Sort, Pagination } from '../../../../common/http_api/infra_ml'; + +// TODO: Reassess validity of this against ML docs +const TIEBREAKER_FIELD = '_doc'; + +const sortToMlFieldMap = { + dataset: 'partition_field_value', + anomalyScore: 'record_score', + startTime: 'timestamp', +}; + +export const createMetricsK8sAnomaliesQuery = ( + jobIds: string[], + startTime: number, + endTime: number, + sort: Sort, + pagination: Pagination +) => { + const { field } = sort; + const { pageSize } = pagination; + + const filters = [ + ...createJobIdsFilters(jobIds), + ...createTimeRangeFilters(startTime, endTime), + ...createResultTypeFilters(['record']), + ]; + + const sourceFields = [ + 'job_id', + 'record_score', + 'typical', + 'actual', + 'partition_field_value', + 'timestamp', + 'bucket_span', + 'by_field_value', + ]; + + const { querySortDirection, queryCursor } = parsePaginationCursor(sort, pagination); + + const sortOptions = [ + { [sortToMlFieldMap[field]]: querySortDirection }, + { [TIEBREAKER_FIELD]: querySortDirection }, // Tiebreaker + ]; + + const resultsQuery = { + ...defaultRequestParameters, + body: { + query: { + bool: { + filter: filters, + }, + }, + search_after: queryCursor, + sort: sortOptions, + size: pageSize, + _source: sourceFields, + }, + }; + + return resultsQuery; +}; + +export const metricsK8sAnomalyHitRT = rt.type({ + _id: rt.string, + _source: rt.intersection([ + rt.type({ + job_id: rt.string, + record_score: rt.number, + typical: rt.array(rt.number), + actual: rt.array(rt.number), + // partition_field_value: rt.string, + bucket_span: rt.number, + timestamp: rt.number, + }), + rt.partial({ + by_field_value: rt.string, + }), + ]), + sort: rt.tuple([rt.union([rt.string, rt.number]), rt.union([rt.string, rt.number])]), +}); + +export type MetricsK8sAnomalyHit = rt.TypeOf; + +export const metricsK8sAnomaliesResponseRT = rt.intersection([ + commonSearchSuccessResponseFieldsRT, + rt.type({ + hits: rt.type({ + hits: rt.array(metricsK8sAnomalyHitRT), + }), + }), +]); + +export type MetricsK8sAnomaliesResponseRT = rt.TypeOf; + +const parsePaginationCursor = (sort: Sort, pagination: Pagination) => { + const { cursor } = pagination; + const { direction } = sort; + + if (!cursor) { + return { querySortDirection: direction, queryCursor: undefined }; + } + + // We will always use ES's search_after to paginate, to mimic "search_before" behaviour we + // need to reverse the user's chosen search direction for the ES query. + if ('searchBefore' in cursor) { + return { + querySortDirection: direction === 'desc' ? 'asc' : 'desc', + queryCursor: cursor.searchBefore, + }; + } else { + return { querySortDirection: direction, queryCursor: cursor.searchAfter }; + } +}; diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/ml_jobs.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/ml_jobs.ts new file mode 100644 index 0000000000000..ee4ccbfaeb5a7 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/ml_jobs.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as rt from 'io-ts'; + +export const createMlJobsQuery = (jobIds: string[]) => ({ + method: 'GET', + path: `/_ml/anomaly_detectors/${jobIds.join(',')}`, + query: { + allow_no_jobs: true, + }, +}); + +export const mlJobRT = rt.type({ + job_id: rt.string, + custom_settings: rt.unknown, +}); + +export const mlJobsResponseRT = rt.type({ + jobs: rt.array(mlJobRT), +}); diff --git a/x-pack/plugins/infra/server/routes/infra_ml/index.ts b/x-pack/plugins/infra/server/routes/infra_ml/index.ts new file mode 100644 index 0000000000000..38684cb22e237 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/infra_ml/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './results'; diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/index.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/index.ts new file mode 100644 index 0000000000000..82e30291faa20 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/infra_ml/results/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './metrics_hosts_anomalies'; +export * from './metrics_k8s_anomalies'; diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts new file mode 100644 index 0000000000000..29122ae159cdc --- /dev/null +++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { InfraBackendLibs } from '../../../lib/infra_types'; +import { + INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH, + getMetricsHostsAnomaliesSuccessReponsePayloadRT, + getMetricsHostsAnomaliesRequestPayloadRT, + GetMetricsHostsAnomaliesRequestPayload, + Sort, + Pagination, +} from '../../../../common/http_api/infra_ml'; +import { createValidationFunction } from '../../../../common/runtime_types'; +import { assertHasInfraMlPlugins } from '../../../utils/request_context'; + +import { isMlPrivilegesError } from '../../../lib/infra_ml/errors'; +import { getMetricsHostsAnomalies } from '../../../lib/infra_ml'; + +export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => { + framework.registerRoute( + { + method: 'post', + path: INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH, + validate: { + body: createValidationFunction(getMetricsHostsAnomaliesRequestPayloadRT), + }, + }, + framework.router.handleLegacyErrors(async (requestContext, request, response) => { + const { + data: { + sourceId, + timeRange: { startTime, endTime }, + sort: sortParam, + pagination: paginationParam, + }, + } = request.body; + + const { sort, pagination } = getSortAndPagination(sortParam, paginationParam); + + try { + assertHasInfraMlPlugins(requestContext); + + const { + data: anomalies, + paginationCursors, + hasMoreEntries, + timing, + } = await getMetricsHostsAnomalies( + requestContext, + sourceId, + startTime, + endTime, + sort, + pagination + ); + + // console.log('---- anomalies', anomalies); + + return response.ok({ + body: getMetricsHostsAnomaliesSuccessReponsePayloadRT.encode({ + data: { + anomalies, + hasMoreEntries, + paginationCursors, + }, + timing, + }), + }); + } catch (error) { + if (Boom.isBoom(error)) { + throw error; + } + + if (isMlPrivilegesError(error)) { + return response.customError({ + statusCode: 403, + body: { + message: error.message, + }, + }); + } + + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + }) + ); +}; + +const getSortAndPagination = ( + sort: Partial = {}, + pagination: Partial = {} +): { + sort: Sort; + pagination: Pagination; +} => { + const sortDefaults = { + field: 'anomalyScore' as const, + direction: 'desc' as const, + }; + + const sortWithDefaults = { + ...sortDefaults, + ...sort, + }; + + const paginationDefaults = { + pageSize: 50, + }; + + const paginationWithDefaults = { + ...paginationDefaults, + ...pagination, + }; + + return { sort: sortWithDefaults, pagination: paginationWithDefaults }; +}; diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts new file mode 100644 index 0000000000000..5260c55836c59 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; +import { InfraBackendLibs } from '../../../lib/infra_types'; +import { + INFA_ML_GET_METRICS_K8S_ANOMALIES_PATH, + getMetricsK8sAnomaliesSuccessReponsePayloadRT, + getMetricsK8sAnomaliesRequestPayloadRT, + GetMetricsK8sAnomaliesRequestPayload, + Sort, + Pagination, +} from '../../../../common/http_api/infra_ml'; +import { createValidationFunction } from '../../../../common/runtime_types'; +import { assertHasInfraMlPlugins } from '../../../utils/request_context'; +import { getMetricK8sAnomalies } from '../../../lib/infra_ml'; +import { isMlPrivilegesError } from '../../../lib/infra_ml/errors'; + +export const initGetK8sAnomaliesRoute = ({ framework }: InfraBackendLibs) => { + framework.registerRoute( + { + method: 'post', + path: INFA_ML_GET_METRICS_K8S_ANOMALIES_PATH, + validate: { + body: createValidationFunction(getMetricsK8sAnomaliesRequestPayloadRT), + }, + }, + framework.router.handleLegacyErrors(async (requestContext, request, response) => { + const { + data: { + sourceId, + timeRange: { startTime, endTime }, + sort: sortParam, + pagination: paginationParam, + }, + } = request.body; + + const { sort, pagination } = getSortAndPagination(sortParam, paginationParam); + + try { + assertHasInfraMlPlugins(requestContext); + + const { + data: anomalies, + paginationCursors, + hasMoreEntries, + timing, + } = await getMetricK8sAnomalies( + requestContext, + sourceId, + startTime, + endTime, + sort, + pagination + ); + + return response.ok({ + body: getMetricsK8sAnomaliesSuccessReponsePayloadRT.encode({ + data: { + anomalies, + hasMoreEntries, + paginationCursors, + }, + timing, + }), + }); + } catch (error) { + if (Boom.isBoom(error)) { + throw error; + } + + if (isMlPrivilegesError(error)) { + return response.customError({ + statusCode: 403, + body: { + message: error.message, + }, + }); + } + + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + }) + ); +}; + +const getSortAndPagination = ( + sort: Partial = {}, + pagination: Partial = {} +): { + sort: Sort; + pagination: Pagination; +} => { + const sortDefaults = { + field: 'anomalyScore' as const, + direction: 'desc' as const, + }; + + const sortWithDefaults = { + ...sortDefaults, + ...sort, + }; + + const paginationDefaults = { + pageSize: 50, + }; + + const paginationWithDefaults = { + ...paginationDefaults, + ...pagination, + }; + + return { sort: sortWithDefaults, pagination: paginationWithDefaults }; +}; From 0db3159a9fe960c6f15c2db5216bf7bc1502dff8 Mon Sep 17 00:00:00 2001 From: Constance Date: Wed, 23 Sep 2020 11:36:13 -0700 Subject: [PATCH 61/92] [Enterprise Search] Move LicenseContext to Kea (#78231) * Fix licensing to use start service + refactor - I noticed my IDE complaining that we were using LicensingPluginSetup (deprecated) instead of LicensingPluginStart, and decided to factor plugin.ts to DRY out / ensure all the dependencies we were passing on app mount were start services and not setup - The number of args we were passing to renderApp was getting a little ridiculous, so I created small helpers to group them up by type (Kibana's args (dependencies/services) vs our plugin's args (data, config, etc.) + bonus remove unused CoreStart type/arg * Add LicensingLogic + mount - replaces useObservable with a manual subscription that updates the license value/state - moves hasXLicense checks to selectors vs helper functions * Update components w/ license checks to use LicensingLogic * Update tests for components now calling LicensingLogic - Add mockLicensingValues to basic kea mock - Minor comment update to mockAllValues obj that I forgot to add in a previous PR * :fire: Remove old LicensingContext --- .../public/applications/__mocks__/index.ts | 2 +- .../public/applications/__mocks__/kea.mock.ts | 4 + ...ontext.mock.ts => licensing_logic.mock.ts} | 4 +- .../__mocks__/mount_with_context.mock.tsx | 6 +- .../__mocks__/shallow_usecontext.mock.ts | 3 +- .../engine_overview/engine_overview.test.tsx | 6 +- .../engine_overview/engine_overview.tsx | 10 +- .../public/applications/index.test.tsx | 30 ++-- .../public/applications/index.tsx | 30 ++-- .../applications/shared/licensing/index.ts | 3 +- .../shared/licensing/license_checks.test.ts | 54 ------ .../shared/licensing/license_checks.ts | 17 -- .../shared/licensing/license_context.test.tsx | 24 --- .../shared/licensing/license_context.tsx | 29 ---- .../shared/licensing/licensing_logic.test.ts | 161 ++++++++++++++++++ .../shared/licensing/licensing_logic.ts | 82 +++++++++ .../shared/not_found/not_found.test.tsx | 14 +- .../shared/not_found/not_found.tsx | 9 +- .../enterprise_search/public/plugin.ts | 54 +++--- 19 files changed, 338 insertions(+), 204 deletions(-) rename x-pack/plugins/enterprise_search/public/applications/__mocks__/{license_context.mock.ts => licensing_logic.mock.ts} (79%) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx delete mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts index f66235ff44c6a..88a900f69c5ec 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts @@ -6,7 +6,7 @@ export { mockHistory, mockLocation } from './react_router_history.mock'; export { mockKibanaContext } from './kibana_context.mock'; -export { mockLicenseContext } from './license_context.mock'; +export { mockLicensingValues } from './licensing_logic.mock'; export { mockHttpValues } from './http_logic.mock'; export { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock'; export { mockAllValues, mockAllActions, setMockValues } from './kea.mock'; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts index 8e6b0baa5fc00..bad6beaa1652e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts @@ -5,13 +5,17 @@ */ /** + * Combine all shared mock values/actions into a single obj + * * NOTE: These variable names MUST start with 'mock*' in order for * Jest to accept its use within a jest.mock() */ +import { mockLicensingValues } from './licensing_logic.mock'; import { mockHttpValues } from './http_logic.mock'; import { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock'; export const mockAllValues = { + ...mockLicensingValues, ...mockHttpValues, ...mockFlashMessagesValues, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/license_context.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/licensing_logic.mock.ts similarity index 79% rename from x-pack/plugins/enterprise_search/public/applications/__mocks__/license_context.mock.ts rename to x-pack/plugins/enterprise_search/public/applications/__mocks__/licensing_logic.mock.ts index 7c37ecc7cde1b..51b32e7a877b2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/license_context.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/licensing_logic.mock.ts @@ -6,6 +6,8 @@ import { licensingMock } from '../../../../licensing/public/mocks'; -export const mockLicenseContext = { +export const mockLicensingValues = { license: licensingMock.createLicense(), + hasPlatinumLicense: false, + hasGoldLicense: false, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx index 5e56f17c8e7f3..646c3104c286f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx @@ -15,8 +15,6 @@ import { getContext, resetContext } from 'kea'; import { I18nProvider } from '@kbn/i18n/react'; import { KibanaContext } from '../'; import { mockKibanaContext } from './kibana_context.mock'; -import { LicenseContext } from '../shared/licensing'; -import { mockLicenseContext } from './license_context.mock'; /** * This helper mounts a component with all the contexts/providers used @@ -34,9 +32,7 @@ export const mountWithContext = (children: React.ReactNode, context?: object) => return mount( - - {children} - + {children} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts index 3a2193db646de..df9e58994e36b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts @@ -9,11 +9,10 @@ * Jest to accept its use within a jest.mock() */ import { mockKibanaContext } from './kibana_context.mock'; -import { mockLicenseContext } from './license_context.mock'; jest.mock('react', () => ({ ...(jest.requireActual('react') as object), - useContext: jest.fn(() => ({ ...mockKibanaContext, ...mockLicenseContext })), + useContext: jest.fn(() => ({ ...mockKibanaContext })), useEffect: jest.fn((fn) => fn()), // Calls on mount/every update - use mount for more complex behavior })); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx index 928d92d791094..44afce96c1a6c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx @@ -82,9 +82,11 @@ describe('EngineOverview', () => { describe('when on a platinum license', () => { it('renders a 2nd meta engines table & makes a 2nd meta engines API call', async () => { - const wrapper = await mountWithAsyncContext(, { - license: { type: 'platinum', isActive: true }, + setMockValues({ + hasPlatinumLicense: true, + http: { ...mockHttpValues.http, get: mockApi }, }); + const wrapper = await mountWithAsyncContext(); expect(wrapper.find(EngineTable)).toHaveLength(2); expect(mockApi).toHaveBeenNthCalledWith(2, '/api/app_search/engines', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx index c0aedbe7dc6b4..0cb9ba106dbb8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useValues } from 'kea'; import { EuiPageContent, @@ -19,7 +19,7 @@ import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chro import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { FlashMessages } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; -import { LicenseContext, ILicenseContext, hasPlatinumLicense } from '../../../shared/licensing'; +import { LicensingLogic } from '../../../shared/licensing'; import { EngineIcon } from './assets/engine_icon'; import { MetaEngineIcon } from './assets/meta_engine_icon'; @@ -40,7 +40,7 @@ interface ISetEnginesCallbacks { export const EngineOverview: React.FC = () => { const { http } = useValues(HttpLogic); - const { license } = useContext(LicenseContext) as ILicenseContext; + const { hasPlatinumLicense } = useValues(LicensingLogic); const [isLoading, setIsLoading] = useState(true); const [engines, setEngines] = useState([]); @@ -72,13 +72,13 @@ export const EngineOverview: React.FC = () => { }, [enginesPage]); useEffect(() => { - if (hasPlatinumLicense(license)) { + if (hasPlatinumLicense) { const params = { type: 'meta', pageIndex: metaEnginesPage }; const callbacks = { setResults: setMetaEngines, setResultsTotal: setMetaEnginesTotal }; setEnginesData(params, callbacks); } - }, [license, metaEnginesPage]); + }, [hasPlatinumLicense, metaEnginesPage]); if (isLoading) return ; if (!engines.length) return ; diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx index 053c450ab925e..6ee63ee22cae2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx @@ -6,7 +6,6 @@ import React from 'react'; -import { AppMountParameters } from 'src/core/public'; import { coreMock } from 'src/core/public/mocks'; import { licensingMock } from '../../../licensing/public/mocks'; @@ -15,37 +14,38 @@ import { AppSearch } from './app_search'; import { WorkplaceSearch } from './workplace_search'; describe('renderApp', () => { - let params: AppMountParameters; - const core = coreMock.createStart(); - const plugins = { - licensing: licensingMock.createSetup(), + const kibanaDeps = { + params: coreMock.createAppMountParamters(), + core: coreMock.createStart(), + plugins: { licensing: licensingMock.createStart() }, + } as any; + const pluginData = { + config: {}, + data: {}, } as any; - const config = {}; - const data = {} as any; beforeEach(() => { jest.clearAllMocks(); - params = coreMock.createAppMountParamters(); }); it('mounts and unmounts UI', () => { const MockApp = () =>
Hello world!
; - const unmount = renderApp(MockApp, params, core, plugins, config, data); - expect(params.element.querySelector('.hello-world')).not.toBeNull(); + const unmount = renderApp(MockApp, kibanaDeps, pluginData); + expect(kibanaDeps.params.element.querySelector('.hello-world')).not.toBeNull(); unmount(); - expect(params.element.innerHTML).toEqual(''); + expect(kibanaDeps.params.element.innerHTML).toEqual(''); }); it('renders AppSearch', () => { - renderApp(AppSearch, params, core, plugins, config, data); - expect(params.element.querySelector('.setupGuide')).not.toBeNull(); + renderApp(AppSearch, kibanaDeps, pluginData); + expect(kibanaDeps.params.element.querySelector('.setupGuide')).not.toBeNull(); }); it('renders WorkplaceSearch', () => { - renderApp(WorkplaceSearch, params, core, plugins, config, data); - expect(params.element.querySelector('.setupGuide')).not.toBeNull(); + renderApp(WorkplaceSearch, kibanaDeps, pluginData); + expect(kibanaDeps.params.element.querySelector('.setupGuide')).not.toBeNull(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 0869ef7b22729..4a25ecf6067cc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -14,8 +14,8 @@ import { getContext, resetContext } from 'kea'; import { I18nProvider } from '@kbn/i18n/react'; import { AppMountParameters, CoreStart, ApplicationStart, ChromeBreadcrumb } from 'src/core/public'; -import { ClientConfigType, ClientData, PluginsSetup } from '../plugin'; -import { LicenseProvider } from './shared/licensing'; +import { PluginsStart, ClientConfigType, ClientData } from '../plugin'; +import { mountLicensingLogic } from './shared/licensing'; import { mountHttpLogic } from './shared/http'; import { mountFlashMessagesLogic } from './shared/flash_messages'; import { IExternalUrl } from './shared/enterprise_search_url'; @@ -39,15 +39,18 @@ export const KibanaContext = React.createContext({}); export const renderApp = ( App: React.FC, - params: AppMountParameters, - core: CoreStart, - plugins: PluginsSetup, - config: ClientConfigType, - { externalUrl, errorConnecting, ...initialData }: ClientData + { params, core, plugins }: { params: AppMountParameters; core: CoreStart; plugins: PluginsStart }, + { config, data }: { config: ClientConfigType; data: ClientData } ) => { + const { externalUrl, errorConnecting, ...initialData } = data; + resetContext({ createStore: true }); const store = getContext().store as Store; + const unmountLicensingLogic = mountLicensingLogic({ + license$: plugins.licensing.license$, + }); + const unmountHttpLogic = mountHttpLogic({ http: core.http, errorConnecting, @@ -67,19 +70,18 @@ export const renderApp = ( setDocTitle: core.chrome.docTitle.change, }} > - - - - - - - + + + + + , params.element ); return () => { ReactDOM.unmountComponentAtNode(params.element); + unmountLicensingLogic(); unmountHttpLogic(); unmountFlashMessagesLogic(); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts index 29c11ffa1cef8..4e371b337c40a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { LicenseContext, LicenseProvider, ILicenseContext } from './license_context'; -export { hasPlatinumLicense, hasGoldLicense } from './license_checks'; +export { LicensingLogic, mountLicensingLogic } from './licensing_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts deleted file mode 100644 index 40f0f6380c21c..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts +++ /dev/null @@ -1,54 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { hasPlatinumLicense, hasGoldLicense } from './license_checks'; - -describe('hasPlatinumLicense', () => { - it('is true for platinum licenses', () => { - expect(hasPlatinumLicense({ isActive: true, type: 'platinum' } as any)).toEqual(true); - }); - - it('is true for enterprise licenses', () => { - expect(hasPlatinumLicense({ isActive: true, type: 'enterprise' } as any)).toEqual(true); - }); - - it('is true for trial licenses', () => { - expect(hasPlatinumLicense({ isActive: true, type: 'platinum' } as any)).toEqual(true); - }); - - it('is false if the current license is expired', () => { - expect(hasPlatinumLicense({ isActive: false, type: 'platinum' } as any)).toEqual(false); - expect(hasPlatinumLicense({ isActive: false, type: 'enterprise' } as any)).toEqual(false); - expect(hasPlatinumLicense({ isActive: false, type: 'trial' } as any)).toEqual(false); - }); - - it('is false for licenses below platinum', () => { - expect(hasPlatinumLicense({ isActive: true, type: 'basic' } as any)).toEqual(false); - expect(hasPlatinumLicense({ isActive: false, type: 'standard' } as any)).toEqual(false); - expect(hasPlatinumLicense({ isActive: true, type: 'gold' } as any)).toEqual(false); - }); -}); - -describe('hasGoldLicense', () => { - it('is true for gold+ and trial licenses', () => { - expect(hasGoldLicense({ isActive: true, type: 'gold' } as any)).toEqual(true); - expect(hasGoldLicense({ isActive: true, type: 'platinum' } as any)).toEqual(true); - expect(hasGoldLicense({ isActive: true, type: 'enterprise' } as any)).toEqual(true); - expect(hasGoldLicense({ isActive: true, type: 'trial' } as any)).toEqual(true); - }); - - it('is false if the current license is expired', () => { - expect(hasGoldLicense({ isActive: false, type: 'gold' } as any)).toEqual(false); - expect(hasGoldLicense({ isActive: false, type: 'platinum' } as any)).toEqual(false); - expect(hasGoldLicense({ isActive: false, type: 'enterprise' } as any)).toEqual(false); - expect(hasGoldLicense({ isActive: false, type: 'trial' } as any)).toEqual(false); - }); - - it('is false for licenses below gold', () => { - expect(hasGoldLicense({ isActive: true, type: 'basic' } as any)).toEqual(false); - expect(hasGoldLicense({ isActive: false, type: 'standard' } as any)).toEqual(false); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts deleted file mode 100644 index d13d0909243be..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ILicense } from '../../../../../licensing/public'; - -export const hasPlatinumLicense = (license?: ILicense) => { - const qualifyingLicenses = ['platinum', 'enterprise', 'trial']; - return license?.isActive && qualifyingLicenses.includes(license?.type as string); -}; - -export const hasGoldLicense = (license?: ILicense) => { - const qualifyingLicenses = ['gold', 'platinum', 'enterprise', 'trial']; - return license?.isActive && qualifyingLicenses.includes(license?.type as string); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx deleted file mode 100644 index c65474ec1f590..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useContext } from 'react'; - -import { mountWithContext } from '../../__mocks__'; -import { LicenseContext, ILicenseContext } from './'; - -describe('LicenseProvider', () => { - const MockComponent: React.FC = () => { - const { license } = useContext(LicenseContext) as ILicenseContext; - return
{license?.type}
; - }; - - it('renders children', () => { - const wrapper = mountWithContext(, { license: { type: 'basic' } }); - - expect(wrapper.find('.license-test')).toHaveLength(1); - expect(wrapper.text()).toEqual('basic'); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx deleted file mode 100644 index 9b47959ff7544..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import useObservable from 'react-use/lib/useObservable'; -import { Observable } from 'rxjs'; - -import { ILicense } from '../../../../../licensing/public'; - -export interface ILicenseContext { - license: ILicense; -} -interface ILicenseContextProps { - license$: Observable; - children: React.ReactNode; -} - -export const LicenseContext = React.createContext({}); - -export const LicenseProvider: React.FC = ({ license$, children }) => { - // Listen for changes to license subscription - const license = useObservable(license$); - - // Render rest of application and pass down license via context - return ; -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts new file mode 100644 index 0000000000000..153a5ae765468 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resetContext } from 'kea'; +import { BehaviorSubject } from 'rxjs'; + +import { licensingMock } from '../../../../../licensing/public/mocks'; + +import { LicensingLogic, mountLicensingLogic } from './licensing_logic'; + +describe('LicensingLogic', () => { + const mockLicense = licensingMock.createLicense(); + const mockLicense$ = new BehaviorSubject(mockLicense); + const mount = () => mountLicensingLogic({ license$: mockLicense$ }); + + beforeEach(() => { + jest.clearAllMocks(); + resetContext({}); + }); + + describe('setLicense()', () => { + it('sets license value', () => { + mount(); + LicensingLogic.actions.setLicense('test' as any); + expect(LicensingLogic.values.license).toEqual('test'); + }); + }); + + describe('setLicenseSubscription()', () => { + it('sets licenseSubscription value', () => { + mount(); + LicensingLogic.actions.setLicenseSubscription('test' as any); + expect(LicensingLogic.values.licenseSubscription).toEqual('test'); + }); + }); + + describe('licensing subscription', () => { + describe('on mount', () => { + it('subscribes to the license observable', () => { + mount(); + expect(LicensingLogic.values.license).toEqual(mockLicense); + expect(LicensingLogic.values.licenseSubscription).not.toBeNull(); + }); + }); + + describe('on subscription update', () => { + it('updates the license value', () => { + mount(); + + const nextMockLicense = licensingMock.createLicense({ license: { status: 'invalid' } }); + mockLicense$.next(nextMockLicense); + + expect(LicensingLogic.values.license).toEqual(nextMockLicense); + }); + }); + + describe('on unmount', () => { + it('unsubscribes to the license observable', () => { + const mockUnsubscribe = jest.fn(); + const unmount = mountLicensingLogic({ + license$: { subscribe: () => ({ unsubscribe: mockUnsubscribe }) } as any, + }); + unmount(); + expect(mockUnsubscribe).toHaveBeenCalled(); + }); + + it('does not crash if no subscription exists', () => { + const unmount = mount(); + LicensingLogic.actions.setLicenseSubscription(null as any); + unmount(); + }); + }); + }); + + describe('license check selectors', () => { + beforeEach(() => { + mount(); + }); + + const updateLicense = (license: any) => { + const updatedLicense = licensingMock.createLicense({ license }); + mockLicense$.next(updatedLicense); + }; + + describe('hasPlatinumLicense', () => { + it('is true for platinum+ and trial licenses', () => { + updateLicense({ status: 'active', type: 'platinum' }); + expect(LicensingLogic.values.hasPlatinumLicense).toEqual(true); + + updateLicense({ status: 'active', type: 'enterprise' }); + expect(LicensingLogic.values.hasPlatinumLicense).toEqual(true); + + updateLicense({ status: 'active', type: 'trial' }); + expect(LicensingLogic.values.hasPlatinumLicense).toEqual(true); + }); + + it('is false if the current license is expired', () => { + updateLicense({ status: 'expired', type: 'platinum' }); + expect(LicensingLogic.values.hasPlatinumLicense).toEqual(false); + + updateLicense({ status: 'expired', type: 'enterprise' }); + expect(LicensingLogic.values.hasPlatinumLicense).toEqual(false); + + updateLicense({ status: 'expired', type: 'trial' }); + expect(LicensingLogic.values.hasPlatinumLicense).toEqual(false); + }); + + it('is false for licenses below platinum', () => { + updateLicense({ status: 'active', type: 'basic' }); + expect(LicensingLogic.values.hasPlatinumLicense).toEqual(false); + + updateLicense({ status: 'active', type: 'standard' }); + expect(LicensingLogic.values.hasPlatinumLicense).toEqual(false); + + updateLicense({ status: 'active', type: 'gold' }); + expect(LicensingLogic.values.hasPlatinumLicense).toEqual(false); + }); + }); + + describe('hasGoldLicense', () => { + it('is true for gold+ and trial licenses', () => { + updateLicense({ status: 'active', type: 'gold' }); + expect(LicensingLogic.values.hasGoldLicense).toEqual(true); + + updateLicense({ status: 'active', type: 'platinum' }); + expect(LicensingLogic.values.hasGoldLicense).toEqual(true); + + updateLicense({ status: 'active', type: 'enterprise' }); + expect(LicensingLogic.values.hasGoldLicense).toEqual(true); + + updateLicense({ status: 'active', type: 'trial' }); + expect(LicensingLogic.values.hasGoldLicense).toEqual(true); + }); + + it('is false if the current license is expired', () => { + updateLicense({ status: 'expired', type: 'gold' }); + expect(LicensingLogic.values.hasGoldLicense).toEqual(false); + + updateLicense({ status: 'expired', type: 'platinum' }); + expect(LicensingLogic.values.hasGoldLicense).toEqual(false); + + updateLicense({ status: 'expired', type: 'enterprise' }); + expect(LicensingLogic.values.hasGoldLicense).toEqual(false); + + updateLicense({ status: 'expired', type: 'trial' }); + expect(LicensingLogic.values.hasGoldLicense).toEqual(false); + }); + + it('is false for licenses below gold', () => { + updateLicense({ status: 'active', type: 'basic' }); + expect(LicensingLogic.values.hasGoldLicense).toEqual(false); + + updateLicense({ status: 'active', type: 'standard' }); + expect(LicensingLogic.values.hasGoldLicense).toEqual(false); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts new file mode 100644 index 0000000000000..ae31b2ec6168a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kea, MakeLogicType } from 'kea'; +import { Observable, Subscription } from 'rxjs'; + +import { ILicense } from '../../../../../licensing/public'; + +export interface ILicensingValues { + license: ILicense | null; + licenseSubscription: Subscription | null; + hasPlatinumLicense: boolean; + hasGoldLicense: boolean; +} +export interface ILicensingActions { + setLicense(license: ILicense): ILicense; + setLicenseSubscription(licenseSubscription: Subscription): Subscription; +} + +export const LicensingLogic = kea>({ + path: ['enterprise_search', 'licensing_logic'], + actions: { + setLicense: (license) => license, + setLicenseSubscription: (licenseSubscription) => licenseSubscription, + }, + reducers: { + license: [ + null, + { + setLicense: (_, license) => license, + }, + ], + licenseSubscription: [ + null, + { + setLicenseSubscription: (_, licenseSubscription) => licenseSubscription, + }, + ], + }, + selectors: { + hasPlatinumLicense: [ + (selectors) => [selectors.license], + (license) => { + const qualifyingLicenses = ['platinum', 'enterprise', 'trial']; + return license?.isActive && qualifyingLicenses.includes(license?.type); + }, + ], + hasGoldLicense: [ + (selectors) => [selectors.license], + (license) => { + const qualifyingLicenses = ['gold', 'platinum', 'enterprise', 'trial']; + return license?.isActive && qualifyingLicenses.includes(license?.type); + }, + ], + }, + events: ({ props, actions, values }) => ({ + afterMount: () => { + const licenseSubscription = props.license$.subscribe(async (license: ILicense) => { + actions.setLicense(license); + }); + actions.setLicenseSubscription(licenseSubscription); + }, + beforeUnmount: () => { + if (values.licenseSubscription) values.licenseSubscription.unsubscribe(); + }, + }), +}); + +/** + * Mount/props helper + */ +interface ILicensingLogicProps { + license$: Observable; +} +export const mountLicensingLogic = (props: ILicensingLogicProps) => { + LicensingLogic(props); + const unmount = LicensingLogic.mount(); + return unmount; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx index ce9071ad7b9d0..62c0af31cffd9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../__mocks__/shallow_usecontext.mock'; +import '../../__mocks__/kea.mock'; -import React, { useContext } from 'react'; +import React from 'react'; +import { useValues } from 'kea'; import { shallow } from 'enzyme'; import { EuiButton as EuiButtonExternal, EuiEmptyPrompt } from '@elastic/eui'; @@ -18,13 +19,6 @@ import { WorkplaceSearchLogo } from './assets/workplace_search_logo'; import { NotFound } from './'; describe('NotFound', () => { - const basicLicense = { isActive: true, type: 'basic' }; - const goldLicense = { isActive: true, type: 'gold' }; - - beforeEach(() => { - (useContext as jest.Mock).mockImplementation(() => ({ license: basicLicense })); - }); - it('renders an App Search 404 view', () => { const wrapper = shallow(); const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow(); @@ -50,7 +44,7 @@ describe('NotFound', () => { }); it('changes the support URL if the user has a gold+ license', () => { - (useContext as jest.Mock).mockImplementation(() => ({ license: goldLicense })); + (useValues as jest.Mock).mockReturnValueOnce({ hasGoldLicense: true }); const wrapper = shallow(); const prompt = wrapper.find(EuiEmptyPrompt).dive().shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx index bd988854225fb..40bb5efcc6330 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; +import { useValues } from 'kea'; import { i18n } from '@kbn/i18n'; import { EuiPageContent, @@ -24,7 +25,7 @@ import { import { EuiButton } from '../react_router_helpers'; import { SetAppSearchChrome, SetWorkplaceSearchChrome } from '../kibana_chrome'; import { SendAppSearchTelemetry, SendWorkplaceSearchTelemetry } from '../telemetry'; -import { LicenseContext, ILicenseContext, hasGoldLicense } from '../licensing'; +import { LicensingLogic } from '../licensing'; import { AppSearchLogo } from './assets/app_search_logo'; import { WorkplaceSearchLogo } from './assets/workplace_search_logo'; @@ -39,8 +40,8 @@ interface NotFoundProps { } export const NotFound: React.FC = ({ product = {} }) => { - const { license } = useContext(LicenseContext) as ILicenseContext; - const supportUrl = hasGoldLicense(license) ? LICENSED_SUPPORT_URL : product.SUPPORT_URL; + const { hasGoldLicense } = useValues(LicensingLogic); + const supportUrl = hasGoldLicense ? LICENSED_SUPPORT_URL : product.SUPPORT_URL; let Logo; let SetPageChrome; diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index c23bb23be3979..f59ec830c812f 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -7,7 +7,6 @@ import { AppMountParameters, CoreSetup, - CoreStart, HttpSetup, Plugin, PluginInitializerContext, @@ -17,7 +16,7 @@ import { FeatureCatalogueCategory, HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; -import { LicensingPluginSetup } from '../../licensing/public'; +import { LicensingPluginStart } from '../../licensing/public'; import { APP_SEARCH_PLUGIN, ENTERPRISE_SEARCH_PLUGIN, @@ -36,7 +35,9 @@ export interface ClientData extends IInitialAppData { export interface PluginsSetup { home?: HomePublicPluginSetup; - licensing: LicensingPluginSetup; +} +export interface PluginsStart { + licensing: LicensingPluginStart; } export class EnterpriseSearchPlugin implements Plugin { @@ -57,16 +58,17 @@ export class EnterpriseSearchPlugin implements Plugin { appRoute: ENTERPRISE_SEARCH_PLUGIN.URL, category: DEFAULT_APP_CATEGORIES.enterpriseSearch, mount: async (params: AppMountParameters) => { - const [coreStart] = await core.getStartServices(); - const { chrome } = coreStart; - chrome.docTitle.change(ENTERPRISE_SEARCH_PLUGIN.NAME); + const kibanaDeps = await this.getKibanaDeps(core, params); + const pluginData = this.getPluginData(); - await this.getInitialData(coreStart.http); + const { chrome, http } = kibanaDeps.core; + chrome.docTitle.change(ENTERPRISE_SEARCH_PLUGIN.NAME); + await this.getInitialData(http); const { renderApp } = await import('./applications'); const { EnterpriseSearch } = await import('./applications/enterprise_search'); - return renderApp(EnterpriseSearch, params, coreStart, plugins, this.config, this.data); + return renderApp(EnterpriseSearch, kibanaDeps, pluginData); }, }); @@ -77,16 +79,17 @@ export class EnterpriseSearchPlugin implements Plugin { appRoute: APP_SEARCH_PLUGIN.URL, category: DEFAULT_APP_CATEGORIES.enterpriseSearch, mount: async (params: AppMountParameters) => { - const [coreStart] = await core.getStartServices(); - const { chrome } = coreStart; - chrome.docTitle.change(APP_SEARCH_PLUGIN.NAME); + const kibanaDeps = await this.getKibanaDeps(core, params); + const pluginData = this.getPluginData(); - await this.getInitialData(coreStart.http); + const { chrome, http } = kibanaDeps.core; + chrome.docTitle.change(APP_SEARCH_PLUGIN.NAME); + await this.getInitialData(http); const { renderApp } = await import('./applications'); const { AppSearch } = await import('./applications/app_search'); - return renderApp(AppSearch, params, coreStart, plugins, this.config, this.data); + return renderApp(AppSearch, kibanaDeps, pluginData); }, }); @@ -97,11 +100,12 @@ export class EnterpriseSearchPlugin implements Plugin { appRoute: WORKPLACE_SEARCH_PLUGIN.URL, category: DEFAULT_APP_CATEGORIES.enterpriseSearch, mount: async (params: AppMountParameters) => { - const [coreStart] = await core.getStartServices(); - const { chrome } = coreStart; - chrome.docTitle.change(WORKPLACE_SEARCH_PLUGIN.NAME); + const kibanaDeps = await this.getKibanaDeps(core, params); + const pluginData = this.getPluginData(); - await this.getInitialData(coreStart.http); + const { chrome, http } = kibanaDeps.core; + chrome.docTitle.change(APP_SEARCH_PLUGIN.NAME); + await this.getInitialData(http); const { renderApp, renderHeaderActions } = await import('./applications'); const { WorkplaceSearch } = await import('./applications/workplace_search'); @@ -113,7 +117,7 @@ export class EnterpriseSearchPlugin implements Plugin { renderHeaderActions(WorkplaceSearchHeaderActions, element, this.data.externalUrl) ); - return renderApp(WorkplaceSearch, params, coreStart, plugins, this.config, this.data); + return renderApp(WorkplaceSearch, kibanaDeps, pluginData); }, }); @@ -149,10 +153,22 @@ export class EnterpriseSearchPlugin implements Plugin { } } - public start(core: CoreStart) {} + public start() {} public stop() {} + private async getKibanaDeps(core: CoreSetup, params: AppMountParameters) { + // Helper for using start dependencies on mount (instead of setup dependencies) + // and for grouping Kibana-related args together (vs. plugin-specific args) + const [coreStart, pluginsStart] = await core.getStartServices(); + return { params, core: coreStart, plugins: pluginsStart as PluginsStart }; + } + + private getPluginData() { + // Small helper for grouping plugin data related args together + return { config: this.config, data: this.data }; + } + private async getInitialData(http: HttpSetup) { if (!this.config.host) return; // No API to call if (this.hasInitialized) return; // We've already made an initial call From 792db1726adce8a2232edfb831bab7bd9966ff98 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Wed, 23 Sep 2020 11:52:04 -0700 Subject: [PATCH 62/92] [DOCS] Redirects CCR and Remote Clusters docs to ES reference (#77909) * [DOCS] Redirects CRR and Remote Clusters docs to ES reference * [DOCS] Updates link for remote clusters --- docs/management/images/add_remote_cluster.png | Bin 254288 -> 0 bytes .../management/images/auto_follow_pattern.png | Bin 273420 -> 0 bytes .../cross-cluster-replication-list-view.png | Bin 117230 -> 0 bytes .../images/remote-clusters-list-view.png | Bin 96631 -> 0 bytes docs/management/managing-ccr.asciidoc | 80 ------------------ .../managing-remote-clusters.asciidoc | 50 ----------- docs/redirects.asciidoc | 12 +++ docs/user/management.asciidoc | 8 +- 8 files changed, 14 insertions(+), 136 deletions(-) delete mode 100755 docs/management/images/add_remote_cluster.png delete mode 100755 docs/management/images/auto_follow_pattern.png delete mode 100755 docs/management/images/cross-cluster-replication-list-view.png delete mode 100755 docs/management/images/remote-clusters-list-view.png delete mode 100644 docs/management/managing-ccr.asciidoc delete mode 100644 docs/management/managing-remote-clusters.asciidoc diff --git a/docs/management/images/add_remote_cluster.png b/docs/management/images/add_remote_cluster.png deleted file mode 100755 index 160d29b741c6293151973078123cf5eab5996e00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 254288 zcmeFYc{J4T|2M8w+UPBn&`{ahP`1KYN|CIklD(33EHTD5GbEx771?Kyt-_FHFpMcG zyRi?38A5|GW*7{{jQiF5`~7@A-`~04zrXKupL08NoHNVonrmLy^Ljj=&&T6&y|A({ z7TznlS3p2O*yQ@vTLJcfNf>Vv66aKE0R4?ihq@6Nx(pSX6-;OpL#ACI33 z)8*nG{2aEv|K#!xN@*1a7VyZ8sLY|vAQqNNg$~H6)9-w9FmS>`2W8k^hI%p&_51#+vId3 z`^mE@+kbxdQ_-&jq^*_tm}&LD{%QIj%a7)Xq4hg-)rl>ErLq4!$crMyT4u$;4_kCw zkPjNYXs|Ha{)YT%_Fd*Xg=z=7=0Eh@mDY>C9T{8f=j& z)PFg|(jW=jg*!DqzB0LPP)4sT9=lU}?z#ZaRK47pP9w4d}zDMow_Dhn}WVM zMMsN6L-U2u^WGEd`2C4Wke_BDBL5lgl-h3M9^$rtzmVP0eilrcukC+E)7D3xo6!3AI|9dr zCba(N^Q6#dS~TrH?_2#AMgPyQG)OZJvG||g9Bio}9`k&x7 zu)X$wAed9WvA}9-)^{-uN}UZJo<$FujBoUo*vl`3bfNYoIw3cP<6WD6cK&E^7FTw7 z!x!Y2F-V^WAfa;tr&JT9b>9sXk%rMaO~&!bYim0!{=Mx(kAqdBCPHuAc&QHI=Wj-A zg?HUmsb9urYC-Qx>xVmhh<9&}zaL6i6~elt@o60osilHKBCSFzQbVhd&1e%aOyE#j zaq&3`^^jCXZC~DSN`U6_F~xu3;uG*+Xkg}>%V`x$%&q!yQ8c78Rjd8AnTiS4hkQbV zS_;vL_v~{�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�yP;87%zA{Pjk$mPZw8@DX0Ed8y?X0z*~%?7 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 z%0A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 -qBF8;j$Wu|JsSbZ}=H5@5 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 diff --git a/docs/management/images/cross-cluster-replication-list-view.png b/docs/management/images/cross-cluster-replication-list-view.png deleted file mode 100755 index 4c45174cff7f12f8b066b9d62ed1c3762ccec02e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 117230 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 h Data > Cross-Cluster Replication*. - -[role="screenshot"] -image::images/cross-cluster-replication-list-view.png[][Cross-cluster replication list view] - -[float] -=== Prerequisites - -* You must have a {ref}/modules-remote-clusters.html[remote cluster]. -* Leader indices must meet {ref}/ccr-requirements.html[these requirements]. -* The Elasticsearch version of the local cluster must be the same as or newer than the remote cluster. -Refer to {ref}/ccr-overview.html[this document] for more information. - -[float] -=== Required permissions - -The `manage` and `manage_ccr` cluster privileges are required to access *Cross-Cluster Replication*. - -You can add these privileges in *Stack Management > Security > Roles*. - -[float] -[[configure-replication]] -=== Configure replication - -Replication requires a leader index, the index being replicated, and a -follower index, which will contain the leader index's replicated data. -The follower index is passive in that it can read requests and searches, -but cannot accept direct writes. Only the leader index is active for direct writes. - -You can configure follower indices in two ways: - -* Create specific follower indices -* Create follower indices from an auto-follow pattern - -[float] -==== Create specific follower indices - -To replicate data from existing indices, or set up local followers on a case-by-case basis, -go to *Follower indices*. When you create the follower index, you must reference the -remote cluster and the leader index that you created in the remote cluster. - -[role="screenshot"] -image::images/follower_indices.png[][UI for adding follower indices] - -[float] -==== Create follower indices from an auto-follow pattern - -To automatically detect and follow new indices when they are created on a remote cluster, -go to *Auto-follow patterns*. Creating an auto-follow pattern is useful when you have -time series data, like event logs, on the remote cluster that is created or rolled over on a daily basis. - -When creating the pattern, you must reference the remote cluster that you -connected to your local cluster. You must also specify a collection of index patterns -that match the indices you want to automatically follow. - -Once you configure an -auto-follow pattern, any time a new index with a name that matches the pattern is -created in the remote cluster, a follower index is automatically configured in the local cluster. - -[role="screenshot"] -image::images/auto_follow_pattern.png[UI for adding an auto-follow pattern] - -[float] -[[manage-replication]] -=== Manage replication - -Use the list views in *Cross-Cluster Replication* to monitor whether the replication is active and -pause and resume replication. You can also edit and remove the follower indices and auto-follow patterns. - -For an example of cross-cluster replication, -refer to https://www.elastic.co/blog/bi-directional-replication-with-elasticsearch-cross-cluster-replication-ccr[Bi-directional replication with Elasticsearch cross-cluster replication]. diff --git a/docs/management/managing-remote-clusters.asciidoc b/docs/management/managing-remote-clusters.asciidoc deleted file mode 100644 index 92e0fa822b056..0000000000000 --- a/docs/management/managing-remote-clusters.asciidoc +++ /dev/null @@ -1,50 +0,0 @@ -[[working-remote-clusters]] -== Remote Clusters - -Use *Remote Clusters* to establish a unidirectional -connection from your cluster to other clusters. This functionality is -required for {ref}/xpack-ccr.html[cross-cluster replication] and -{ref}/modules-cross-cluster-search.html[cross-cluster search]. - -To get started, open the menu, then go to *Stack Management > Data > Remote Clusters*. - -[role="screenshot"] -image::images/remote-clusters-list-view.png[Remote Clusters list view, including Add a remote cluster button] - -[float] -=== Required permissions - -The `manage` cluster privilege is required to access *Remote Clusters*. - -You can add this privilege in *Stack Management > Security > Roles*. - -[float] -[[managing-remote-clusters]] -=== Add a remote cluster - -A {ref}/modules-remote-clusters.html[remote cluster] connection works by configuring a remote cluster and -connecting to a limited number of nodes, called {ref}/modules-remote-clusters.html#sniff-mode[seed nodes], -in that cluster. -Alternatively, you can define a single proxy address for the remote cluster. - -By default, a cross-cluster request, such as a cross-cluster search or -replication request, fails if any cluster in the request is unavailable. -To skip a cluster when its unavailable, -set *Skip if unavailable* to true. - -Once you add a remote cluster, you can configure <> -to reproduce indices in the remote cluster on a local cluster. - -[role="screenshot"] -image::images/add_remote_cluster.png[][UI for adding a remote cluster] - -To create an index pattern to search across clusters, -use the same syntax that you’d use in a raw cross-cluster search request in {es}: :. -See <> for examples. - -[float] -[[manage-remote-clusters]] -=== Manage remote clusters - -From the *Remote Clusters* list view, you can drill down into each cluster and -view its status. You can also edit and delete a cluster. diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 42d1d89145d79..5067bc08bec99 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -100,3 +100,15 @@ This content has moved to the <> page. == TSVB This page was deleted. See <>. + +[role="exclude",id="managing-cross-cluster-replication"] +== Cross-Cluster Replication + +This content has moved. See +{ref}/ccr-getting-started.html[Set up cross-cluster replication]. + +[role="exclude",id="working-remote-clusters"] +== Remote clusters + +This content has moved. See +{ref}/ccr-getting-started.html#ccr-getting-started-remote-cluster[Connect to a remote cluster]. diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index bc96463f6efba..e0d550a15a907 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -58,12 +58,12 @@ years of historical data in combination with your raw data. | {ref}/transforms.html[Transforms] |Use transforms to pivot existing {es} indices into summarized or entity-centric indices. -| <> +| {ref}/ccr-getting-started.html[Cross-Cluster Replication] |Replicate indices on a remote cluster and copy them to a follower index on a local cluster. This is important for disaster recovery. It also keeps data local for faster queries. -| <> +| {ref}/ccr-getting-started.html#ccr-getting-started-remote-cluster[Remote Clusters] |Manage your remote clusters for use with cross-cluster search and cross-cluster replication. You can add and remove remote clusters, and check their connectivity. |=== @@ -180,8 +180,6 @@ include::{kib-repo-dir}/management/alerting/connector-management.asciidoc[] include::{kib-repo-dir}/management/managing-beats.asciidoc[] -include::{kib-repo-dir}/management/managing-ccr.asciidoc[] - include::{kib-repo-dir}/management/index-lifecycle-policies/intro-to-lifecycle-policies.asciidoc[] include::{kib-repo-dir}/management/index-lifecycle-policies/create-policy.asciidoc[] @@ -200,8 +198,6 @@ include::{kib-repo-dir}/management/managing-licenses.asciidoc[] include::{kib-repo-dir}/management/numeral.asciidoc[] -include::{kib-repo-dir}/management/managing-remote-clusters.asciidoc[] - include::{kib-repo-dir}/management/rollups/create_and_manage_rollups.asciidoc[] include::{kib-repo-dir}/management/managing-saved-objects.asciidoc[] From f9e0679682fd0e0fbe3b5664adc72da8d15ada8b Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 23 Sep 2020 13:53:16 -0500 Subject: [PATCH 63/92] [Metrics UI] Minor fixes to inventory timeline (#78226) --- .../components/waffle/interval_label.tsx | 2 +- .../inventory_view/hooks/use_timeline.ts | 39 ++++++++++++------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx index 6e031c8396f07..dbbfb0f49c0e9 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx @@ -22,7 +22,7 @@ export const IntervalLabel = ({ intervalAsString }: Props) => {

    diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_timeline.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_timeline.ts index 650eda0362d9e..acf9011ac7ddd 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_timeline.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_timeline.ts @@ -28,21 +28,28 @@ const ONE_MINUTE = 60; const ONE_HOUR = ONE_MINUTE * 60; const ONE_DAY = ONE_HOUR * 24; const ONE_WEEK = ONE_DAY * 7; +const ONE_MONTH = ONE_DAY * 30; + +const getDisplayInterval = (interval: string | undefined) => { + if (interval) { + const intervalInSeconds = getIntervalInSeconds(interval); + if (intervalInSeconds < 300) return '5m'; + } + return interval; +}; const getTimeLengthFromInterval = (interval: string | undefined) => { if (interval) { const intervalInSeconds = getIntervalInSeconds(interval); - const multiplier = - intervalInSeconds < ONE_MINUTE - ? ONE_HOUR / intervalInSeconds - : intervalInSeconds < ONE_HOUR - ? 60 - : intervalInSeconds < ONE_DAY - ? 7 - : intervalInSeconds < ONE_WEEK - ? 30 - : 1; - const timeLength = intervalInSeconds * multiplier; + // Get up to 288 datapoints based on interval + const timeLength = + intervalInSeconds <= ONE_MINUTE * 15 + ? ONE_DAY + : intervalInSeconds <= ONE_MINUTE * 35 + ? ONE_DAY * 3 + : intervalInSeconds <= ONE_HOUR * 2.5 + ? ONE_WEEK + : ONE_MONTH; return { timeLength, intervalInSeconds }; } else { return { timeLength: 0, intervalInSeconds: 0 }; @@ -67,15 +74,19 @@ export function useTimeline( ); }; - const timeLengthResult = useMemo(() => getTimeLengthFromInterval(interval), [interval]); + const displayInterval = useMemo(() => getDisplayInterval(interval), [interval]); + + const timeLengthResult = useMemo(() => getTimeLengthFromInterval(displayInterval), [ + displayInterval, + ]); const { timeLength, intervalInSeconds } = timeLengthResult; const timerange: InfraTimerangeInput = { - interval: interval ?? '', + interval: displayInterval ?? '', to: currentTime + intervalInSeconds * 1000, from: currentTime - timeLength * 1000, - lookbackSize: 0, ignoreLookback: true, + forceInterval: true, }; const { error, loading, response, makeRequest } = useHTTPRequest( From abb1cbfa5f8ae2d387e70b312b709c3abb83b706 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 23 Sep 2020 12:35:54 -0700 Subject: [PATCH 64/92] skip flaky suite (#39842) --- test/functional/apps/discover/_inspector.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/functional/apps/discover/_inspector.js b/test/functional/apps/discover/_inspector.js index 900ad28e14e69..fcb66fbd52cf7 100644 --- a/test/functional/apps/discover/_inspector.js +++ b/test/functional/apps/discover/_inspector.js @@ -34,7 +34,8 @@ export default function ({ getService, getPageObjects }) { return hitsCountStatsRow[STATS_ROW_VALUE_INDEX]; } - describe('inspect', () => { + // FLAKY: https://github.com/elastic/kibana/issues/39842 + describe.skip('inspect', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('discover'); From a54cc17f0f4e7c364985fc54b48a6f6b1369e28c Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 23 Sep 2020 12:42:16 -0700 Subject: [PATCH 65/92] skip flaky suite (#61612) --- .../security_solution/cypress/integration/url_state.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts index 6c1d73492f30a..04aecfab4561f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts @@ -51,7 +51,8 @@ const ABSOLUTE_DATE = { startTimeTimeline: '2019-08-02T20:03:29.186Z', }; -describe('url state', () => { +// FLAKY: https://github.com/elastic/kibana/issues/61612 +describe.skip('url state', () => { it('sets the global start and end dates from the url', () => { loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.url); cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON).should( From 94a4e38053aa38738405fab246ed703459fda7b7 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Wed, 23 Sep 2020 15:49:52 -0400 Subject: [PATCH 66/92] [Security Solution] Options to select index patterns (#77192) * init commit * lots of cleanup * starting on tests... problems * Ready for review * remove sample data * remove comment and fix type * pr changes * fix type * scratchy * sourcerer in timeline * sourcerer in timeline * wip * moving to redux * working on types * fixed * more adjustments, tests fixed * FF off * pr ready * renaming * url state working, hoc not working * url state working for timeline and default scope * script to build fields for beat doc * refactor sourcerer * refactor host to useSourcerer * refactor network to useSourcerer * refactor overview to useSourcerer * refactor detections to useSourcerer * wip for timelines to remove all useSource * wip indexes timeline * do component tests * start container tests * start container tests * update selection widget of index patterns + remove last useWithSource * add indexeNames in network kpi * fix type errors * fix type * missing merge master * get existing index from config file * fixing broken tests * add saving button to avoid to many queries to be aborted * reducer timeline tests broke * need to rewind * much better * timeline saving index names + clean up url state to only manage default * more test fixing * more test changes * remove all the useWithSource + deprecated the graphql until we delete it in a new PR + delete all the beat doc * default timeline to all index when creation + filter index patterns to make sure you do not add one who we do not know * fix types * test for stateful timeline render * we should not have change that * no chnages + snapshot * fix test + bugs from review * fix uncommon processes indexNames * review III * change design for main page of the sourcerer from design * bug fixes when opening old timeline + implementation of new design * fix circular deps * remove unused attributes for event details * design cleanup * fix api integration test with the new search strategy * add reset + manage accordion state * fix bugs + types issues * cleanup * update docs * review -> remove tooltip when popover is open * cypress fixing * fix for ml_condition_links and url_state cypress tests * add cy wait for race condition in pagination tests * missing plumbing kpi host Co-authored-by: Steph Milovic Co-authored-by: Patryk Kopycinski --- ...ic.indexpatternsservice.getidswithtitle.md | 16 + ...lugins-data-public.indexpatternsservice.md | 1 + .../index_patterns/index_patterns.ts | 19 + src/plugins/data/public/public.api.md | 4 + .../security_solution/common/constants.ts | 3 +- .../common/search_strategy/common/index.ts | 2 +- .../search_strategy/index_fields/index.ts | 81 + .../timeline/events/details/index.ts | 1 - .../common/types/timeline/index.ts | 3 + .../integration/ml_conditional_links.spec.ts | 26 +- .../cypress/integration/pagination.spec.ts | 1 + .../cypress/integration/url_state.spec.ts | 12 +- x-pack/plugins/security_solution/package.json | 1 + .../security_solution/public/app/app.tsx | 18 +- .../public/app/home/index.tsx | 30 +- .../components/case_header_page/index.tsx | 4 +- .../cases/components/case_view/index.tsx | 1 + .../components/alerts_viewer/alerts_table.tsx | 2 + .../common/components/alerts_viewer/index.tsx | 2 + .../common/components/alerts_viewer/types.ts | 2 +- .../drag_drop_context_wrapper.test.tsx | 19 +- .../drag_and_drop/draggable_wrapper.test.tsx | 51 +- .../draggable_wrapper_hover_content.test.tsx | 21 +- .../draggable_wrapper_hover_content.tsx | 27 +- .../drag_and_drop/droppable_wrapper.test.tsx | 45 +- .../events_viewer/events_viewer.test.tsx | 344 +- .../events_viewer/events_viewer.tsx | 4 +- .../components/events_viewer/index.test.tsx | 36 +- .../common/components/events_viewer/index.tsx | 29 +- .../add_exception_modal/index.test.tsx | 15 +- .../exceptions/add_exception_modal/index.tsx | 15 +- .../edit_exception_modal/index.test.tsx | 11 +- .../exceptions/edit_exception_modal/index.tsx | 14 +- .../common/components/header_global/index.tsx | 10 +- .../__snapshots__/index.test.tsx.snap | 3 + .../common/components/header_page/index.tsx | 5 + .../components/last_event_time/index.test.tsx | 8 +- .../components/last_event_time/index.tsx | 83 +- .../matrix_histogram/index.test.tsx | 1 + .../components/matrix_histogram/index.tsx | 5 +- .../components/matrix_histogram/types.ts | 2 +- .../navigation/breadcrumbs/index.test.ts | 31 +- .../common/components/navigation/helpers.ts | 11 +- .../components/navigation/index.test.tsx | 3 + .../common/components/navigation/index.tsx | 10 +- .../navigation/tab_navigation/index.test.tsx | 2 + .../navigation/tab_navigation/index.tsx | 13 +- .../navigation/tab_navigation/types.ts | 2 + .../common/components/navigation/types.ts | 2 + .../components/sourcerer/index.test.tsx | 192 +- .../common/components/sourcerer/index.tsx | 301 +- .../common/components/sourcerer/selectors.tsx | 35 + .../components/sourcerer/translations.ts | 37 +- .../public/common/components/top_n/helpers.ts | 8 +- .../common/components/top_n/index.test.tsx | 93 +- .../public/common/components/top_n/index.tsx | 7 +- .../common/components/top_n/top_n.test.tsx | 109 +- .../public/common/components/top_n/top_n.tsx | 16 +- .../common/components/url_state/constants.ts | 1 + .../common/components/url_state/helpers.ts | 20 + .../components/url_state/index.test.tsx | 2 +- .../url_state/index_mocked.test.tsx | 45 +- .../url_state/initialize_redux_by_url.tsx | 20 +- .../components/url_state/test_dependencies.ts | 1 + .../common/components/url_state/types.ts | 9 + .../anomalies_query_tab_body/index.tsx | 2 + .../anomalies_query_tab_body/types.ts | 1 + .../events/last_event_time/index.ts | 23 +- .../containers/matrix_histogram/index.ts | 22 +- .../common/containers/query_template.tsx | 1 + .../containers/source/index.gql_query.ts | 31 - .../common/containers/source/index.test.tsx | 109 - .../public/common/containers/source/index.tsx | 329 +- .../public/common/containers/source/mock.ts | 625 +- .../common/containers/source/translations.ts | 21 + .../common/containers/sourcerer/constants.ts | 24 +- .../containers/sourcerer/format.test.tsx | 23 - .../common/containers/sourcerer/format.ts | 96 - .../containers/sourcerer/index.test.tsx | 368 +- .../common/containers/sourcerer/index.tsx | 450 +- .../common/containers/sourcerer/mocks.ts | 98 +- .../common/lib/kibana/__mocks__/index.ts | 20 +- .../public/common/mock/global_state.ts | 27 + .../public/common/mock/index_pattern.ts | 6 +- .../public/common/mock/timeline_results.ts | 2 + .../public/common/store/actions.ts | 1 + .../public/common/store/model.ts | 1 + .../public/common/store/reducer.ts | 14 +- .../public/common/store/selectors.ts | 1 + .../public/common/store/sourcerer/actions.ts | 36 + .../public/common/store/sourcerer/index.ts | 12 + .../public/common/store/sourcerer/model.ts | 86 + .../public/common/store/sourcerer/reducer.ts | 93 + .../common/store/sourcerer/selectors.ts | 91 + .../public/common/store/types.ts | 2 + .../components/alerts_table/actions.test.tsx | 1 + .../components/alerts_table/actions.tsx | 2 + .../components/alerts_table/index.test.tsx | 1 - .../components/alerts_table/index.tsx | 27 +- .../timeline_actions/alert_context_menu.tsx | 42 +- .../investigate_in_timeline_action.tsx | 3 +- .../detection_engine_header_page/index.tsx | 2 +- .../rules/step_about_rule/index.test.tsx | 10 +- .../rules/step_about_rule/index.tsx | 8 +- .../rules/step_define_rule/index.tsx | 7 +- .../detections/components/user_info/index.tsx | 24 +- .../rules/fetch_index_patterns.test.tsx | 475 - .../rules/fetch_index_patterns.tsx | 132 - .../detection_engine/rules/index.ts | 1 - .../detection_engine.test.tsx | 6 +- .../detection_engine/detection_engine.tsx | 9 +- .../rules/details/index.test.tsx | 6 +- .../detection_engine/rules/details/index.tsx | 12 +- .../public/graphql/introspection.json | 326 +- .../security_solution/public/graphql/types.ts | 125 +- .../first_last_seen_host/index.test.tsx | 49 +- .../components/first_last_seen_host/index.tsx | 75 +- .../__snapshots__/index.test.tsx.snap | 91 - .../components/hosts_table/index.test.tsx | 3 - .../hosts/components/hosts_table/index.tsx | 3 - .../kpi_hosts/authentications/index.tsx | 2 + .../components/kpi_hosts/hosts/index.tsx | 2 + .../hosts/components/kpi_hosts/index.tsx | 9 +- .../hosts/components/kpi_hosts/types.ts | 1 + .../components/kpi_hosts/unique_ips/index.tsx | 2 + .../containers/authentications/index.tsx | 12 +- .../hosts/containers/hosts/details/_index.tsx | 18 +- .../hosts/containers/hosts/details/index.tsx | 11 +- .../hosts/first_last_seen/index.tsx | 18 +- .../public/hosts/containers/hosts/index.tsx | 12 +- .../containers/kpi_host_details/index.tsx | 16 +- .../kpi_hosts/authentications/index.tsx | 12 +- .../containers/kpi_hosts/hosts/index.tsx | 12 +- .../containers/kpi_hosts/unique_ips/index.tsx | 12 +- .../containers/uncommon_processes/index.tsx | 12 +- .../hosts/pages/details/details_tabs.test.tsx | 1 + .../hosts/pages/details/details_tabs.tsx | 8 +- .../public/hosts/pages/details/index.tsx | 16 +- .../public/hosts/pages/details/types.ts | 1 + .../public/hosts/pages/hosts.test.tsx | 14 +- .../public/hosts/pages/hosts.tsx | 15 +- .../public/hosts/pages/hosts_tabs.tsx | 12 +- .../authentications_query_tab_body.tsx | 12 +- .../navigation/events_query_tab_body.tsx | 4 + .../pages/navigation/hosts_query_tab_body.tsx | 5 +- .../public/hosts/pages/navigation/types.ts | 4 +- .../uncommon_process_query_tab_body.tsx | 11 +- .../public/hosts/pages/types.ts | 3 +- .../components/administration_list_page.tsx | 7 +- .../pages/policy/view/policy_details.tsx | 1 + .../trusted_apps_page.test.tsx.snap | 66 + .../view/trusted_apps_page.test.tsx | 1 + .../__snapshots__/index.test.tsx.snap | 1 + .../components/kpi_network/dns/index.tsx | 2 + .../components/kpi_network/index.test.tsx | 5 +- .../network/components/kpi_network/index.tsx | 7 +- .../kpi_network/network_events/index.tsx | 2 + .../kpi_network/tls_handshakes/index.tsx | 2 + .../network/components/kpi_network/types.ts | 1 + .../kpi_network/unique_flows/index.tsx | 2 + .../kpi_network/unique_private_ips/index.tsx | 2 + .../network/containers/details/index.tsx | 12 +- .../containers/kpi_network/dns/index.tsx | 12 +- .../kpi_network/network_events/index.tsx | 12 +- .../kpi_network/tls_handshakes/index.tsx | 12 +- .../kpi_network/unique_flows/index.tsx | 12 +- .../kpi_network/unique_private_ips/index.tsx | 12 +- .../network/containers/network_dns/index.tsx | 12 +- .../network/containers/network_http/index.tsx | 12 +- .../network_top_countries/index.tsx | 12 +- .../containers/network_top_n_flow/index.tsx | 12 +- .../public/network/containers/tls/index.tsx | 12 +- .../network/pages/details/index.test.tsx | 8 +- .../public/network/pages/details/index.tsx | 22 +- .../details/network_http_query_table.tsx | 1 + .../network_top_countries_query_table.tsx | 1 + .../network_top_n_flow_query_table.tsx | 2 + .../network/pages/details/tls_query_table.tsx | 1 + .../public/network/pages/details/types.ts | 1 + .../navigation/countries_query_tab_body.tsx | 2 + .../pages/navigation/dns_query_tab_body.tsx | 3 + .../pages/navigation/http_query_tab_body.tsx | 2 + .../pages/navigation/ips_query_tab_body.tsx | 2 + .../pages/navigation/network_routes.tsx | 2 + .../pages/navigation/tls_query_tab_body.tsx | 2 + .../public/network/pages/navigation/types.ts | 2 + .../public/network/pages/network.test.tsx | 31 +- .../public/network/pages/network.tsx | 20 +- .../alerts_by_category/index.test.tsx | 28 +- .../components/alerts_by_category/index.tsx | 3 + .../components/event_counts/index.test.tsx | 18 +- .../components/event_counts/index.tsx | 4 + .../components/events_by_dataset/index.tsx | 6 +- .../__snapshots__/index.test.tsx.snap | 2 + .../components/host_overview/index.test.tsx | 2 + .../components/host_overview/index.tsx | 12 +- .../components/overview_host/index.test.tsx | 13 +- .../components/overview_host/index.tsx | 9 +- .../overview_network/index.test.tsx | 16 +- .../components/overview_network/index.tsx | 3 + .../recent_cases/no_cases/index.test.tsx | 2 +- .../containers/overview_host/index.tsx | 23 +- .../containers/overview_network/index.tsx | 12 +- .../public/overview/pages/overview.test.tsx | 184 +- .../public/overview/pages/overview.tsx | 21 +- .../public/overview/pages/summary.tsx | 6 +- .../security_solution/public/plugin.tsx | 56 +- .../components/flyout/button/index.tsx | 5 +- .../components/manage_timeline/index.test.tsx | 54 +- .../components/manage_timeline/index.tsx | 39 - .../components/open_timeline/helpers.test.ts | 6 + .../components/open_timeline/helpers.ts | 9 + .../components/open_timeline/index.tsx | 28 +- .../__snapshots__/timeline.test.tsx.snap | 14 +- .../components/timeline/body/helpers.tsx | 9 +- .../components/timeline/body/index.tsx | 8 +- .../timeline/data_providers/helpers.test.tsx | 6 +- .../timeline/data_providers/helpers.tsx | 8 +- .../components/timeline/index.test.tsx | 153 +- .../timelines/components/timeline/index.tsx | 46 +- .../properties/use_create_timeline.test.tsx | 9 +- .../properties/use_create_timeline.tsx | 28 +- .../timeline/search_or_filter/index.tsx | 38 +- .../timeline/search_or_filter/pick_events.tsx | 358 +- .../search_or_filter/search_or_filter.tsx | 14 +- .../timeline/search_or_filter/selectors.tsx | 43 + .../timeline/search_or_filter/translations.ts | 60 +- .../timelines/components/timeline/styles.tsx | 4 +- .../components/timeline/timeline.test.tsx | 33 +- .../components/timeline/timeline.tsx | 46 +- .../timelines/containers/details/index.tsx | 7 +- .../public/timelines/containers/index.tsx | 98 +- .../containers/one/index.gql_query.ts | 1 + .../timelines/containers/persist.gql_query.ts | 1 + .../timelines/pages/timelines_page.test.tsx | 6 +- .../public/timelines/pages/timelines_page.tsx | 10 +- .../timelines/store/timeline/actions.ts | 16 +- .../timelines/store/timeline/defaults.ts | 1 + .../timelines/store/timeline/epic.test.ts | 2 + .../public/timelines/store/timeline/epic.ts | 3 + .../timeline/epic_local_storage.test.tsx | 6 +- .../timelines/store/timeline/helpers.ts | 8 +- .../public/timelines/store/timeline/model.ts | 7 +- .../timelines/store/timeline/reducer.test.ts | 1537 +- .../timelines/store/timeline/reducer.ts | 13 + .../scripts/beat_docs/build.js | 233 + .../graphql/source_status/schema.gql.ts | 2 +- .../server/graphql/timeline/schema.gql.ts | 2 + .../security_solution/server/graphql/types.ts | 265 +- .../server/lib/compose/kibana.ts | 2 +- .../lib/events/elasticsearch_adapter.ts | 3 +- .../lib/index_fields/elasticsearch_adapter.ts | 165 +- .../server/lib/index_fields/index.ts | 5 +- .../server/lib/index_fields/types.ts | 3 +- .../lib/timeline/saved_object_mappings.ts | 3 + .../security_solution/server/plugin.ts | 7 + .../index_fields/index.test.ts} | 230 +- .../search_strategy/index_fields/index.ts | 224 + .../search_strategy/index_fields/mock.ts | 113 + .../timeline/factory/events/all/helpers.ts | 1 + .../factory/events/details/helpers.ts | 3 +- .../utils/beat_schema/8.0.0/auditbeat.ts | 7902 ---- .../server/utils/beat_schema/8.0.0/ecs.ts | 5675 --- .../utils/beat_schema/8.0.0/filebeat.ts | 21243 --------- .../server/utils/beat_schema/8.0.0/index.ts | 37 - .../utils/beat_schema/8.0.0/packetbeat.ts | 8556 ---- .../utils/beat_schema/8.0.0/winlogbeat.ts | 2844 -- .../server/utils/beat_schema/fields.ts | 36118 ++++++++++++++++ .../server/utils/beat_schema/index.test.ts | 397 - .../server/utils/beat_schema/index.ts | 129 - .../server/utils/beat_schema/type.ts | 73 - .../apis/security_solution/sources.ts | 147 +- 272 files changed, 41559 insertions(+), 52569 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md create mode 100644 x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/sourcerer/selectors.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/containers/source/index.gql_query.ts delete mode 100644 x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/containers/source/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/common/containers/sourcerer/format.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/containers/sourcerer/format.ts create mode 100644 x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts create mode 100644 x-pack/plugins/security_solution/public/common/store/sourcerer/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts create mode 100644 x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts create mode 100644 x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts delete mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/selectors.tsx create mode 100644 x-pack/plugins/security_solution/scripts/beat_docs/build.js rename x-pack/plugins/security_solution/server/{lib/index_fields/elasticsearch_adapter.test.ts => search_strategy/index_fields/index.test.ts} (65%) create mode 100644 x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts create mode 100644 x-pack/plugins/security_solution/server/search_strategy/index_fields/mock.ts delete mode 100644 x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/auditbeat.ts delete mode 100644 x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/ecs.ts delete mode 100644 x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/filebeat.ts delete mode 100644 x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/index.ts delete mode 100644 x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/packetbeat.ts delete mode 100644 x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/winlogbeat.ts create mode 100644 x-pack/plugins/security_solution/server/utils/beat_schema/fields.ts delete mode 100644 x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts delete mode 100644 x-pack/plugins/security_solution/server/utils/beat_schema/index.ts delete mode 100644 x-pack/plugins/security_solution/server/utils/beat_schema/type.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md new file mode 100644 index 0000000000000..7d29ced66afa8 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [getIdsWithTitle](./kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md) + +## IndexPatternsService.getIdsWithTitle property + +Get list of index pattern ids with titles + +Signature: + +```typescript +getIdsWithTitle: (refresh?: boolean) => Promise>; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md index 0022bff34a8e7..af087344268d7 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md @@ -29,6 +29,7 @@ export declare class IndexPatternsService | [getFieldsForIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions) => Promise<any> | Get field list by providing an index patttern (or spec) | | [getFieldsForWildcard](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md) | | (options?: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | | [getIds](./kibana-plugin-plugins-data-public.indexpatternsservice.getids.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern ids | +| [getIdsWithTitle](./kibana-plugin-plugins-data-public.indexpatternsservice.getidswithtitle.md) | | (refresh?: boolean) => Promise<Array<{
    id: string;
    title: string;
    }>> | Get list of index pattern ids with titles | | [getTitles](./kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern titles | | [refreshFields](./kibana-plugin-plugins-data-public.indexpatternsservice.refreshfields.md) | | (indexPattern: IndexPattern) => Promise<void> | Refresh field list for a given index pattern | | [savedObjectToSpec](./kibana-plugin-plugins-data-public.indexpatternsservice.savedobjecttospec.md) | | (savedObject: SavedObject<IndexPatternAttributes>) => IndexPatternSpec | Converts index pattern saved object to index pattern spec | diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index c56954ba6a29b..eef8ef10ea754 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -133,6 +133,25 @@ export class IndexPatternsService { return this.savedObjectsCache.map((obj) => obj?.attributes?.title); }; + /** + * Get list of index pattern ids with titles + * @param refresh Force refresh of index pattern list + */ + getIdsWithTitle = async ( + refresh: boolean = false + ): Promise> => { + if (!this.savedObjectsCache || refresh) { + await this.refreshSavedObjectsCache(); + } + if (!this.savedObjectsCache) { + return []; + } + return this.savedObjectsCache.map((obj) => ({ + id: obj?.id, + title: obj?.attributes?.title, + })); + }; + /** * Clear index pattern list cache * @param id optionally clear a single id diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 5919c1e294b2f..ed58ee840a8f8 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1381,6 +1381,10 @@ export class IndexPatternsService { // Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts getFieldsForWildcard: (options?: GetFieldsOptions) => Promise; getIds: (refresh?: boolean) => Promise; + getIdsWithTitle: (refresh?: boolean) => Promise>; getTitles: (refresh?: boolean) => Promise; refreshFields: (indexPattern: IndexPattern) => Promise; savedObjectToSpec: (savedObject: SavedObject) => IndexPatternSpec; diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 9321aa769423f..a93d2817fbbb3 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -11,7 +11,6 @@ export const APP_ICON = 'securityAnalyticsApp'; export const APP_ICON_SOLUTION = 'logoSecurity'; export const APP_PATH = `/app/security`; export const ADD_DATA_PATH = `/app/home#/tutorial_directory/security`; -export const ADD_INDEX_PATH = `/app/management/kibana/indexPatterns/create`; export const DEFAULT_BYTES_FORMAT = 'format:bytes:defaultPattern'; export const DEFAULT_DATE_FORMAT = 'dateFormat'; export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz'; @@ -58,6 +57,8 @@ export const APP_TIMELINES_PATH = `${APP_PATH}/timelines`; export const APP_CASES_PATH = `${APP_PATH}/cases`; export const APP_MANAGEMENT_PATH = `${APP_PATH}/administration`; +export const DETECTIONS_SUB_PLUGIN_ID = `${APP_ID}:${SecurityPageName.detections}`; + /** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */ export const DEFAULT_INDEX_PATTERN = [ 'apm-*-transaction*', diff --git a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts index 48437e12f75a5..0c1f13dac2e69 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/common/index.ts @@ -71,7 +71,7 @@ export interface PaginationInputPaginated { export interface DocValueFields { field: string; - format: string; + format?: string | null; } export interface Explanation { diff --git a/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts b/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts new file mode 100644 index 0000000000000..259a767f8cf70 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +import { IIndexPattern } from 'src/plugins/data/public'; +import { + IEsSearchRequest, + IEsSearchResponse, + IFieldSubType, +} from '../../../../../../src/plugins/data/common'; +import { DocValueFields, Maybe } from '../common'; + +export type BeatFieldsFactoryQueryType = 'beatFields'; + +interface FieldInfo { + category: string; + description?: string; + example?: string | number; + format?: string; + name: string; + type?: string; +} + +export interface IndexField { + /** Where the field belong */ + category: string; + /** Example of field's value */ + example?: Maybe; + /** whether the field's belong to an alias index */ + indexes: Array>; + /** The name of the field */ + name: string; + /** The type of the field's values as recognized by Kibana */ + type: string; + /** Whether the field's values can be efficiently searched for */ + searchable: boolean; + /** Whether the field's values can be aggregated */ + aggregatable: boolean; + /** Description of the field */ + description?: Maybe; + format?: Maybe; + /** the elastic type as mapped in the index */ + esTypes?: string[]; + subType?: IFieldSubType; + readFromDocValues: boolean; +} + +export type BeatFields = Record; + +export interface IndexFieldsStrategyRequest extends IEsSearchRequest { + indices: string[]; + onlyCheckIfIndicesExist: boolean; +} + +export interface IndexFieldsStrategyResponse extends IEsSearchResponse { + indexFields: IndexField[]; + indicesExist: string[]; +} + +export interface BrowserField { + aggregatable: boolean; + category: string; + description: string | null; + example: string | number | null; + fields: Readonly>>; + format: string; + indexes: string[]; + name: string; + searchable: boolean; + type: string; +} + +export type BrowserFields = Readonly>>; + +export const EMPTY_BROWSER_FIELDS = {}; +export const EMPTY_DOCVALUE_FIELD: DocValueFields[] = []; +export const EMPTY_INDEX_PATTERN: IIndexPattern = { + fields: [], + title: '', +}; diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts index 6f9192be40150..9fa7f96599deb 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts @@ -22,7 +22,6 @@ export interface TimelineEventsDetailsStrategyResponse extends IEsSearchResponse export interface TimelineEventsDetailsRequestOptions extends Partial { - defaultIndex: string[]; indexName: string; eventId: string; } diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 84a007e322f11..3888d37a547f7 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -239,6 +239,7 @@ export const SavedTimelineRuntimeType = runtimeTypes.partial({ excludedRowRendererIds: unionWithNullType(runtimeTypes.array(RowRendererIdRuntimeType)), favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)), filters: unionWithNullType(runtimeTypes.array(SavedFilterRuntimeType)), + indexNames: unionWithNullType(runtimeTypes.array(runtimeTypes.string)), kqlMode: unionWithNullType(runtimeTypes.string), kqlQuery: unionWithNullType(SavedFilterQueryQueryRuntimeType), title: unionWithNullType(runtimeTypes.string), @@ -398,3 +399,5 @@ export const importTimelineResultSchema = runtimeTypes.exact( ); export type ImportTimelineResultSchema = runtimeTypes.TypeOf; + +export type TimelineEventsType = 'all' | 'raw' | 'alert' | 'signal' | 'custom'; diff --git a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts index 0b302efd655a8..06a8d3a79c3cd 100644 --- a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts @@ -94,7 +94,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpNullKqlQuery); cy.url().should( 'include', - '/app/security/network/ip/127.0.0.1/source?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' + '/app/security/network/ip/127.0.0.1/source?sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' ); }); @@ -102,7 +102,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpKqlQuery); cy.url().should( 'include', - '/app/security/network/ip/127.0.0.1/source?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' + '/app/security/network/ip/127.0.0.1/source?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' ); }); @@ -110,7 +110,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpNullKqlQuery); cy.url().should( 'include', - 'app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27))' + 'app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%27)&sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27))' ); }); @@ -118,7 +118,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpKqlQuery); cy.url().should( 'include', - '/app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' + '/app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' ); }); @@ -126,7 +126,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkNullKqlQuery); cy.url().should( 'include', - '/app/security/network/flows?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' + '/app/security/network/flows?sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' ); }); @@ -134,7 +134,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkKqlQuery); cy.url().should( 'include', - '/app/security/network/flows?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' + '/app/security/network/flows?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))' ); }); @@ -142,7 +142,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostNullKqlQuery); cy.url().should( 'include', - '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' + '/app/security/hosts/siem-windows/anomalies?sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); @@ -150,7 +150,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQueryVariable); cy.url().should( 'include', - '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' + '/app/security/hosts/siem-windows/anomalies?sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); @@ -158,7 +158,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQuery); cy.url().should( 'include', - '/app/security/hosts/siem-windows/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' + '/app/security/hosts/siem-windows/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); @@ -166,7 +166,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostMultiHostNullKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' + '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%27)&sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); @@ -174,7 +174,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostMultiHostKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' + '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); @@ -182,7 +182,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostVariableHostNullKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' + '/app/security/hosts/anomalies?sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); @@ -190,7 +190,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostVariableHostKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' + '/app/security/hosts/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&sourcerer=(default:!(%27auditbeat-*%27))&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))' ); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/pagination.spec.ts b/x-pack/plugins/security_solution/cypress/integration/pagination.spec.ts index 5dc3182cd9f83..fdccf164c7465 100644 --- a/x-pack/plugins/security_solution/cypress/integration/pagination.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/pagination.spec.ts @@ -35,6 +35,7 @@ describe('Pagination', () => { .then((processNameFirstPage) => { goToThirdPage(); waitForUncommonProcessesToBeLoaded(); + cy.wait(1500); cy.get(PROCESS_NAME_FIELD) .first() .invoke('text') diff --git a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts index 04aecfab4561f..2588c580dedd3 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts @@ -166,7 +166,7 @@ describe.skip('url state', () => { cy.get(NETWORK).should( 'have.attr', 'href', - `/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))` + `/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))` ); }); @@ -179,12 +179,12 @@ describe.skip('url state', () => { cy.get(HOSTS).should( 'have.attr', 'href', - `/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` + `/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); cy.get(NETWORK).should( 'have.attr', 'href', - `/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` + `/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); cy.get(HOSTS_NAMES).first().invoke('text').should('eq', 'siem-kibana'); @@ -195,21 +195,21 @@ describe.skip('url state', () => { cy.get(ANOMALIES_TAB).should( 'have.attr', 'href', - "/app/security/hosts/siem-kibana/anomalies?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))" + "/app/security/hosts/siem-kibana/anomalies?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:!('auditbeat-*'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))" ); cy.get(BREADCRUMBS) .eq(1) .should( 'have.attr', 'href', - `/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` + `/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); cy.get(BREADCRUMBS) .eq(2) .should( 'have.attr', 'href', - `/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` + `/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); }); diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 6982c200a5afd..6d79557fdaa28 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -6,6 +6,7 @@ "license": "Elastic-License", "scripts": { "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js && node ../../../scripts/eslint ./public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix", + "build-beat-doc": "node scripts/beat_docs/build.js && node ../../../scripts/eslint ./server/utils/beat_schema/fields.ts --fix", "build-graphql-types": "node scripts/generate_types_from_graphql.js", "cypress:open": "cypress open --config-file ./cypress/cypress.json", "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/visual_config.ts", diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx index b4e9ba3dd7a71..54b02c374e43f 100644 --- a/x-pack/plugins/security_solution/public/app/app.tsx +++ b/x-pack/plugins/security_solution/public/app/app.tsx @@ -28,8 +28,6 @@ import { ApolloClientContext } from '../common/utils/apollo_context'; import { ManageGlobalTimeline } from '../timelines/components/manage_timeline'; import { StartServices } from '../types'; import { PageRouter } from './routes'; -import { ManageSource } from '../common/containers/sourcerer'; - interface StartAppComponent extends AppFrontendLibs { children: React.ReactNode; history: History; @@ -56,15 +54,13 @@ const StartAppComponent: FC = ({ children, apolloClient, hist - - - - - {children} - - - - + + + + {children} + + + diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index b48ae4e6e2d75..e0dea199e78ff 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import React, { useRef } from 'react'; import styled from 'styled-components'; import { TimelineId } from '../../../common/types/timeline'; @@ -14,11 +14,12 @@ import { HeaderGlobal } from '../../common/components/header_global'; import { HelpMenu } from '../../common/components/help_menu'; import { AutoSaveWarningMsg } from '../../timelines/components/timeline/auto_save_warning'; import { UseUrlState } from '../../common/components/url_state'; -import { useWithSource } from '../../common/containers/source'; import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline'; import { navTabs } from './home_navigations'; -import { useSignalIndex } from '../../detections/containers/detection_engine/alerts/use_signal_index'; -import { useUserInfo } from '../../detections/components/user_info'; +import { useInitSourcerer, useSourcererScope } from '../../common/containers/sourcerer'; +import { useKibana } from '../../common/lib/kibana'; +import { DETECTIONS_SUB_PLUGIN_ID } from '../../../common/constants'; +import { SourcererScopeName } from '../../common/store/sourcerer/model'; const SecuritySolutionAppWrapper = styled.div` display: flex; @@ -42,20 +43,21 @@ interface HomePageProps { } const HomePageComponent: React.FC = ({ children }) => { - const { signalIndexExists, signalIndexName } = useSignalIndex(); + const { application } = useKibana().services; + const subPluginId = useRef(''); - const indexToAdd = useMemo(() => { - if (signalIndexExists && signalIndexName != null) { - return [signalIndexName]; - } - return null; - }, [signalIndexExists, signalIndexName]); + application.currentAppId$.subscribe((appId) => { + subPluginId.current = appId ?? ''; + }); + useInitSourcerer( + subPluginId.current === DETECTIONS_SUB_PLUGIN_ID + ? SourcererScopeName.detections + : SourcererScopeName.default + ); const [showTimeline] = useShowTimeline(); - const { browserFields, indexPattern, indicesExist } = useWithSource('default', indexToAdd); - // side effect: this will attempt to create the signals index if it doesn't exist - useUserInfo(); + const { browserFields, indexPattern, indicesExist } = useSourcererScope(); return ( diff --git a/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx index 0ac6093f2ee04..4f7b17a730b6a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx @@ -9,7 +9,9 @@ import React from 'react'; import { HeaderPage, HeaderPageProps } from '../../../common/components/header_page'; import * as i18n from './translations'; -const CaseHeaderPageComponent: React.FC = (props) => ; +const CaseHeaderPageComponent: React.FC = (props) => ( + +); CaseHeaderPageComponent.defaultProps = { badgeOptions: { diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index b23169af6ceb3..ad113d3e7e737 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -294,6 +294,7 @@ export const CaseComponent = React.memo( = ({ defaultModel={alertsDefaultModel} end={endDate} id={timelineId} + scopeId={SourcererScopeName.default} start={startDate} /> ); diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx index d522e372d7734..0dcd29a2d965b 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx @@ -24,6 +24,7 @@ const AlertsViewComponent: React.FC = ({ deleteQuery, endDate, filterQuery, + indexNames, pageFilters, setQuery, startDate, @@ -62,6 +63,7 @@ const AlertsViewComponent: React.FC = ({ endDate={endDate} filterQuery={filterQuery} id={ID} + indexNames={indexNames} setQuery={setQuery} startDate={startDate} {...alertsHistogramConfigs} diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts index b2637eeb2c65e..280b9111042d0 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/types.ts @@ -15,7 +15,7 @@ type CommonQueryProps = HostsComponentsQueryProps | NetworkComponentQueryProps; export interface AlertsComponentsProps extends Pick< CommonQueryProps, - 'deleteQuery' | 'endDate' | 'filterQuery' | 'skip' | 'setQuery' | 'startDate' + 'deleteQuery' | 'endDate' | 'filterQuery' | 'indexNames' | 'skip' | 'setQuery' | 'startDate' > { timelineId: TimelineIdLiteral; pageFilters: Filter[]; diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx index 9e8bde8d9ff92..eaaba90b35634 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx @@ -6,9 +6,8 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { mockBrowserFields, mocksSource } from '../../containers/source/mock'; +import { mockBrowserFields } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; import { DragDropContextWrapper } from './drag_drop_context_wrapper'; @@ -20,11 +19,9 @@ describe('DragDropContextWrapper', () => { const wrapper = shallow( - - - {message} - - + + {message} + ); expect(wrapper.find('DragDropContextWrapper')).toMatchSnapshot(); @@ -35,11 +32,9 @@ describe('DragDropContextWrapper', () => { const wrapper = mount( - - - {message} - - + + {message} + ); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx index ebfa9ac22bdc7..46e7298677f49 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx @@ -6,11 +6,10 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; import { DraggableStateSnapshot, DraggingStyle } from 'react-beautiful-dnd'; import '../../mock/match_media'; -import { mockBrowserFields, mocksSource } from '../../containers/source/mock'; +import { mockBrowserFields } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; import { mockDataProviders } from '../../../timelines/components/timeline/data_providers/mock/mock_data_providers'; import { DragDropContextWrapper } from './drag_drop_context_wrapper'; @@ -30,11 +29,9 @@ describe('DraggableWrapper', () => { test('it renders against the snapshot', () => { const wrapper = shallow( - - - message} /> - - + + message} /> + ); @@ -44,11 +41,9 @@ describe('DraggableWrapper', () => { test('it renders the children passed to the render prop', () => { const wrapper = mount( - - - message} /> - - + + message} /> + ); @@ -58,11 +53,9 @@ describe('DraggableWrapper', () => { test('it does NOT render hover actions when the mouse is NOT over the draggable wrapper', () => { const wrapper = mount( - - - message} /> - - + + message} /> + ); @@ -72,11 +65,9 @@ describe('DraggableWrapper', () => { test('it renders hover actions when the mouse is over the text of draggable wrapper', () => { const wrapper = mount( - - - message} /> - - + + message} /> + ); @@ -92,11 +83,9 @@ describe('DraggableWrapper', () => { test('it applies text truncation styling when truncate IS specified (implicit: and the user is not dragging)', () => { const wrapper = mount( - - - message} truncate /> - - + + message} truncate /> + ); @@ -108,11 +97,9 @@ describe('DraggableWrapper', () => { test('it does NOT apply text truncation styling when truncate is NOT specified', () => { const wrapper = mount( - - - message} /> - - + + message} /> + ); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index b53da42da55f8..8aa926a36988b 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -8,14 +8,13 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; import { coreMock } from '../../../../../../../src/core/public/mocks'; -import { useWithSource } from '../../containers/source'; import { mockBrowserFields } from '../../containers/source/mock'; import '../../mock/match_media'; import { useKibana } from '../../lib/kibana'; import { TestProviders } from '../../mock'; import { FilterManager } from '../../../../../../../src/plugins/data/public'; import { useAddToTimeline } from '../../hooks/use_add_to_timeline'; - +import { useSourcererScope } from '../../containers/sourcerer'; import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content'; import { ManageGlobalTimeline, @@ -26,12 +25,12 @@ import { TimelineId } from '../../../../common/types/timeline'; jest.mock('../link_to'); jest.mock('../../lib/kibana'); -jest.mock('../../containers/source', () => { - const original = jest.requireActual('../../containers/source'); +jest.mock('../../containers/sourcerer', () => { + const original = jest.requireActual('../../containers/sourcerer'); return { ...original, - useWithSource: jest.fn(), + useSourcererScope: jest.fn(), }; }); @@ -79,8 +78,10 @@ describe('DraggableWrapperHoverContent', () => { beforeAll(() => { // our mock implementation of the useAddToTimeline hook returns a mock startDragToTimeline function: (useAddToTimeline as jest.Mock).mockReturnValue(jest.fn()); - (useWithSource as jest.Mock).mockReturnValue({ + (useSourcererScope as jest.Mock).mockReturnValue({ browserFields: mockBrowserFields, + selectedPatterns: [], + indexPattern: {}, }); }); @@ -203,7 +204,7 @@ describe('DraggableWrapperHoverContent', () => { wrapper = mount( ); @@ -311,7 +312,7 @@ describe('DraggableWrapperHoverContent', () => { {...{ ...defaultProps, onFilterAdded, - timelineId: 'not-active-timeline', + timelineId: TimelineId.test, value: '', }} /> @@ -606,9 +607,7 @@ describe('DraggableWrapperHoverContent', () => { test('filter manager, not active timeline', () => { mount( - + ); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx index a951bfa98d64b..8c68551ddd981 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx @@ -8,7 +8,7 @@ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import React, { useCallback, useMemo, useState, useEffect } from 'react'; import { DraggableId } from 'react-beautiful-dnd'; -import { getAllFieldsByName, useWithSource } from '../../containers/source'; +import { getAllFieldsByName } from '../../containers/source'; import { useAddToTimeline } from '../../hooks/use_add_to_timeline'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { useKibana } from '../../lib/kibana'; @@ -20,6 +20,8 @@ import * as i18n from './translations'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; import { TimelineId } from '../../../../common/types/timeline'; import { SELECTOR_TIMELINE_BODY_CLASS_NAME } from '../../../timelines/components/timeline/styles'; +import { SourcererScopeName } from '../../store/sourcerer/model'; +import { useSourcererScope } from '../../containers/sourcerer'; interface Props { closePopOver?: () => void; @@ -49,7 +51,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ const filterManagerBackup = useMemo(() => kibana.services.data.query.filterManager, [ kibana.services.data.query.filterManager, ]); - const { getManageTimelineById, getTimelineFilterManager } = useManageTimeline(); + const { getTimelineFilterManager } = useManageTimeline(); const filterManager = useMemo( () => @@ -65,13 +67,16 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ // this component is rendered in the context of the active timeline. This // behavior enables the 'All events' view by appending the alerts index // to the index pattern. - const { indexToAdd } = useMemo( - () => - timelineId === TimelineId.active - ? getManageTimelineById(TimelineId.active) - : { indexToAdd: null }, - [getManageTimelineById, timelineId] - ); + const activeScope: SourcererScopeName = + timelineId === TimelineId.active + ? SourcererScopeName.timeline + : timelineId != null && + [TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage].includes( + timelineId as TimelineId + ) + ? SourcererScopeName.detections + : SourcererScopeName.default; + const { browserFields, indexPattern, selectedPatterns } = useSourcererScope(activeScope); const handleStartDragToTimeline = useCallback(() => { startDragToTimeline(); @@ -121,8 +126,6 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ } }, [goGetTimelineId, timelineId]); - const { browserFields, indexPattern } = useWithSource('default', indexToAdd); - return ( <> {!showTopN && value != null && ( @@ -187,7 +190,7 @@ const DraggableWrapperHoverContentComponent: React.FC = ({ browserFields={browserFields} field={field} indexPattern={indexPattern} - indexToAdd={indexToAdd} + indexNames={selectedPatterns} onFilterAdded={onFilterAdded} timelineId={timelineId ?? undefined} toggleTopN={toggleTopN} diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx index bd2f01721290f..14d1c37efb8cf 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.test.tsx @@ -6,9 +6,8 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { mockBrowserFields, mocksSource } from '../../containers/source/mock'; +import { mockBrowserFields } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; import { DragDropContextWrapper } from './drag_drop_context_wrapper'; @@ -24,11 +23,9 @@ describe('DroppableWrapper', () => { const wrapper = shallow( - - - {message} - - + + {message} + ); @@ -40,11 +37,9 @@ describe('DroppableWrapper', () => { const wrapper = mount( - - - {message} - - + + {message} + ); @@ -56,13 +51,11 @@ describe('DroppableWrapper', () => { const wrapper = mount( - - - null} droppableId="testing"> -
    {message}
    -
    -
    -
    + + null} droppableId="testing"> +
    {message}
    +
    +
    ); @@ -72,14 +65,12 @@ describe('DroppableWrapper', () => { test('it renders the render prop contents when a render prop is provided', () => { const wrapper = mount( - - -
    {`isDraggingOver is: ${isDraggingOver}`}
    } - droppableId="testing" - /> -
    -
    + +
    {`isDraggingOver is: ${isDraggingOver}`}
    } + droppableId="testing" + /> +
    ); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 037655f594241..aac1f4f2687eb 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -8,15 +8,13 @@ import React from 'react'; import useResizeObserver from 'use-resize-observer/polyfilled'; import '../../mock/match_media'; -import { mockIndexPattern, TestProviders } from '../../mock'; -// we don't have the types for waitFor just yet, so using "as waitFor" until when we do -import { wait as waitFor } from '@testing-library/react'; +import { mockIndexNames, mockIndexPattern, TestProviders } from '../../mock'; import { mockEventViewerResponse } from './mock'; import { StatefulEventsViewer } from '.'; import { EventsViewer } from './events_viewer'; import { defaultHeaders } from './default_headers'; -import { useFetchIndexPatterns } from '../../../detections/containers/detection_engine/rules/fetch_index_patterns'; +import { useSourcererScope } from '../../containers/sourcerer'; import { mockBrowserFields, mockDocValueFields } from '../../containers/source/mock'; import { eventsDefaultModel } from './default_model'; import { useMountAppended } from '../../utils/use_mount_appended'; @@ -25,6 +23,7 @@ import { TimelineId } from '../../../../common/types/timeline'; import { KqlMode } from '../../../timelines/store/timeline/model'; import { SortDirection } from '../../../timelines/components/timeline/body/sort'; import { AlertsTableFilterGroup } from '../../../detections/components/alerts_table/alerts_filter_group'; +import { SourcererScopeName } from '../../store/sourcerer/model'; import { useTimelineEvents } from '../../../timelines/containers'; jest.mock('../../../timelines/containers', () => ({ @@ -33,8 +32,8 @@ jest.mock('../../../timelines/containers', () => ({ jest.mock('../../components/url_state/normalize_time_range.ts'); -const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock; -jest.mock('../../../detections/containers/detection_engine/rules/fetch_index_patterns'); +const mockUseSourcererScope: jest.Mock = useSourcererScope as jest.Mock; +jest.mock('../../containers/sourcerer'); const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); @@ -45,9 +44,10 @@ const to = '2019-08-27T22:10:56.794Z'; const defaultMocks = { browserFields: mockBrowserFields, - indexPatterns: mockIndexPattern, docValueFields: mockDocValueFields, - isLoading: false, + indexPattern: mockIndexPattern, + loading: false, + selectedPatterns: mockIndexNames, }; const utilityBar = (refetch: inputsModel.Refetch, totalCount: number) => ( @@ -63,6 +63,7 @@ const eventsViewerDefaultProps = { end: to, filters: [], id: TimelineId.detectionsPage, + indexNames: mockIndexNames, indexPattern: mockIndexPattern, isLive: false, isLoadingIndexPattern: false, @@ -79,6 +80,7 @@ const eventsViewerDefaultProps = { columnId: 'foo', sortDirection: 'none' as SortDirection, }, + scopeId: SourcererScopeName.timeline, toggleColumn: jest.fn(), utilityBar, }; @@ -86,154 +88,57 @@ const eventsViewerDefaultProps = { describe('EventsViewer', () => { const mount = useMountAppended(); + let testProps = { + defaultModel: eventsDefaultModel, + end: to, + id: 'test-stateful-events-viewer', + start: from, + scopeId: SourcererScopeName.timeline, + }; + beforeEach(() => { (useTimelineEvents as jest.Mock).mockReturnValue([false, mockEventViewerResponse]); - mockUseFetchIndexPatterns.mockImplementation(() => [{ ...defaultMocks }]); }); - - test('it renders the "Showing..." subtitle with the expected event count', async () => { - const wrapper = mount( - - - - ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().text()).toEqual( - 'Showing: 12 events' - ); - }); + beforeAll(() => { + mockUseSourcererScope.mockImplementation(() => defaultMocks); }); - - test('it does NOT render fetch index pattern is loading', async () => { - mockUseFetchIndexPatterns.mockImplementation(() => [{ ...defaultMocks, isLoading: true }]); - - const wrapper = mount( - - - - ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( - false + describe('rendering', () => { + test('it renders the "Showing..." subtitle with the expected event count', () => { + const wrapper = mount( + + + ); - }); - }); - - test('it does NOT render when start is empty', async () => { - mockUseFetchIndexPatterns.mockImplementation(() => [{ ...defaultMocks, isLoading: true }]); - - const wrapper = mount( - - - - ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( - false + expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().text()).toEqual( + 'Showing: 12 events' ); }); - }); - test('it does NOT render when end is empty', async () => { - mockUseFetchIndexPatterns.mockImplementation(() => [{ ...defaultMocks, isLoading: true }]); - - const wrapper = mount( - - - - ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( - false + test('it renders the Fields Browser as a settings gear', () => { + const wrapper = mount( + + + ); - }); - }); - - test('it renders the Fields Browser as a settings gear', async () => { - const wrapper = mount( - - - - ); - - await waitFor(() => { - wrapper.update(); - expect(wrapper.find(`[data-test-subj="show-field-browser"]`).first().exists()).toBe(true); }); - }); - - test('it renders the footer containing the Load More button', async () => { - const wrapper = mount( - - - - ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="timeline-pagination"]`).first().exists()).toBe(true); - }); - }); - - defaultHeaders.forEach((header) => { - test(`it renders the ${header.id} default EventsViewer column header`, async () => { + // TO DO sourcerer @X + test('it renders the footer containing the pagination', () => { const wrapper = mount( - + ); + expect(wrapper.find(`[data-test-subj="timeline-pagination"]`).first().exists()).toBe(true); + }); - await waitFor(() => { - wrapper.update(); + defaultHeaders.forEach((header) => { + test(`it renders the ${header.id} default EventsViewer column header`, () => { + const wrapper = mount( + + + + ); defaultHeaders.forEach((h) => expect(wrapper.find(`[data-test-subj="header-text-${header.id}"]`).first().exists()).toBe( @@ -242,10 +147,58 @@ describe('EventsViewer', () => { ); }); }); + describe('loading', () => { + beforeAll(() => { + mockUseSourcererScope.mockImplementation(() => ({ ...defaultMocks, loading: true })); + }); + test('it does NOT render fetch index pattern is loading', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( + false + ); + }); + + test('it does NOT render when start is empty', () => { + testProps = { + ...testProps, + start: '', + }; + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( + false + ); + }); + + test('it does NOT render when end is empty', () => { + testProps = { + ...testProps, + end: '', + }; + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( + false + ); + }); + }); }); describe('headerFilterGroup', () => { - test('it renders the provided headerFilterGroup', async () => { + test('it renders the provided headerFilterGroup', () => { const wrapper = mount( { /> ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="alerts-table-filter-group"]`).exists()).toBe(true); - }); + expect(wrapper.find(`[data-test-subj="alerts-table-filter-group"]`).exists()).toBe(true); }); - test('it has a visible HeaderFilterGroupWrapper when Resolver is NOT showing, because graphEventId is undefined', async () => { + test('it has a visible HeaderFilterGroupWrapper when Resolver is NOT showing, because graphEventId is undefined', () => { const wrapper = mount( { /> ); - - await waitFor(() => { - wrapper.update(); - - expect( - wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first() - ).not.toHaveStyleRule('visibility', 'hidden'); - }); + expect( + wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first() + ).not.toHaveStyleRule('visibility', 'hidden'); }); - test('it has a visible HeaderFilterGroupWrapper when Resolver is NOT showing, because graphEventId is an empty string', async () => { + test('it has a visible HeaderFilterGroupWrapper when Resolver is NOT showing, because graphEventId is an empty string', () => { const wrapper = mount( { /> ); - - await waitFor(() => { - wrapper.update(); - - expect( - wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first() - ).not.toHaveStyleRule('visibility', 'hidden'); - }); + expect( + wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first() + ).not.toHaveStyleRule('visibility', 'hidden'); }); - test('it does NOT have a visible HeaderFilterGroupWrapper when Resolver is showing, because graphEventId is a valid id', async () => { + test('it does NOT have a visible HeaderFilterGroupWrapper when Resolver is showing, because graphEventId is a valid id', () => { const wrapper = mount( { /> ); - - await waitFor(() => { - wrapper.update(); - - expect( - wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first() - ).toHaveStyleRule('visibility', 'hidden'); - }); + expect( + wrapper.find(`[data-test-subj="header-filter-group-wrapper"]`).first() + ).toHaveStyleRule('visibility', 'hidden'); }); - test('it (still) renders an invisible headerFilterGroup (to maintain state while hidden) when Resolver is showing, because graphEventId is a valid id', async () => { + test('it (still) renders an invisible headerFilterGroup (to maintain state while hidden) when Resolver is showing, because graphEventId is a valid id', () => { const wrapper = mount( { /> ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="alerts-table-filter-group"]`).exists()).toBe(true); - }); + expect(wrapper.find(`[data-test-subj="alerts-table-filter-group"]`).exists()).toBe(true); }); }); describe('utilityBar', () => { - test('it renders the provided utilityBar when Resolver is NOT showing, because graphEventId is undefined', async () => { + test('it renders the provided utilityBar when Resolver is NOT showing, because graphEventId is undefined', () => { const wrapper = mount( ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(true); - }); + expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(true); }); - test('it renders the provided utilityBar when Resolver is NOT showing, because graphEventId is an empty string', async () => { + test('it renders the provided utilityBar when Resolver is NOT showing, because graphEventId is an empty string', () => { const wrapper = mount( ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(true); - }); + expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(true); }); - test('it does NOT render the provided utilityBar when Resolver is showing, because graphEventId is a valid id', async () => { + test('it does NOT render the provided utilityBar when Resolver is showing, because graphEventId is a valid id', () => { const wrapper = mount( ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(false); - }); + expect(wrapper.find(`[data-test-subj="mock-utility-bar"]`).exists()).toBe(false); }); }); describe('header inspect button', () => { - test('it renders the inspect button when Resolver is NOT showing, because graphEventId is undefined', async () => { + test('it renders the inspect button when Resolver is NOT showing, because graphEventId is undefined', () => { const wrapper = mount( ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(true); - }); + expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(true); }); - test('it renders the inspect button when Resolver is NOT showing, because graphEventId is an empty string', async () => { + test('it renders the inspect button when Resolver is NOT showing, because graphEventId is an empty string', () => { const wrapper = mount( ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(true); - }); + expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(true); }); - test('it does NOT render the inspect button when Resolver is showing, because graphEventId is a valid id', async () => { + test('it does NOT render the inspect button when Resolver is showing, because graphEventId is a valid id', () => { const wrapper = mount( ); - - await waitFor(() => { - wrapper.update(); - - expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(false); - }); + expect(wrapper.find(`[data-test-subj="inspect-icon-button"]`).exists()).toBe(false); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index 2998bd031d674..2c8c8136a4733 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -95,6 +95,7 @@ interface Props { headerFilterGroup?: React.ReactNode; height?: number; id: string; + indexNames: string[]; indexPattern: IIndexPattern; isLive: boolean; isLoadingIndexPattern: boolean; @@ -121,6 +122,7 @@ const EventsViewerComponent: React.FC = ({ filters, headerFilterGroup, id, + indexNames, indexPattern, isLive, isLoadingIndexPattern, @@ -213,7 +215,7 @@ const EventsViewerComponent: React.FC = ({ fields, filterQuery: combinedQueries!.filterQuery, id, - indexPattern, + indexNames, limit: itemsPerPage, sort: sortField, startDate: start, diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index 8c61281422c2a..9a3c0fa1cad2e 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -10,14 +10,13 @@ import useResizeObserver from 'use-resize-observer/polyfilled'; import '../../mock/match_media'; // we don't have the types for waitFor just yet, so using "as waitFor" until when we do import { wait as waitFor } from '@testing-library/react'; -import { mockIndexPattern, TestProviders } from '../../mock'; +import { TestProviders } from '../../mock'; import { useMountAppended } from '../../utils/use_mount_appended'; import { mockEventViewerResponse } from './mock'; import { StatefulEventsViewer } from '.'; -import { useFetchIndexPatterns } from '../../../detections/containers/detection_engine/rules/fetch_index_patterns'; -import { mockBrowserFields } from '../../containers/source/mock'; import { eventsDefaultModel } from './default_model'; +import { SourcererScopeName } from '../../store/sourcerer/model'; import { useTimelineEvents } from '../../../timelines/containers'; jest.mock('../../../timelines/containers', () => ({ @@ -26,15 +25,6 @@ jest.mock('../../../timelines/containers', () => ({ jest.mock('../../components/url_state/normalize_time_range.ts'); -const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock; -jest.mock('../../../detections/containers/detection_engine/rules/fetch_index_patterns'); -mockUseFetchIndexPatterns.mockImplementation(() => [ - { - browserFields: mockBrowserFields, - indexPatterns: mockIndexPattern, - }, -]); - const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); @@ -42,6 +32,14 @@ mockUseResizeObserver.mockImplementation(() => ({})); const from = '2019-08-27T22:10:56.794Z'; const to = '2019-08-26T22:10:56.791Z'; +const testProps = { + defaultModel: eventsDefaultModel, + end: to, + indexNames: [], + id: 'test-stateful-events-viewer', + scopeId: SourcererScopeName.default, + start: from, +}; describe('StatefulEventsViewer', () => { const mount = useMountAppended(); @@ -50,12 +48,7 @@ describe('StatefulEventsViewer', () => { test('it renders the events viewer', async () => { const wrapper = mount( - + ); @@ -70,12 +63,7 @@ describe('StatefulEventsViewer', () => { test('it renders InspectButtonContainer', async () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index e4520dab4626a..cd43c7e493065 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -9,7 +9,6 @@ import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import styled from 'styled-components'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { inputsModel, inputsSelectors, State } from '../../store'; import { inputsActions } from '../../store/actions'; import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline'; @@ -20,11 +19,11 @@ import { } from '../../../timelines/store/timeline/model'; import { OnChangeItemsPerPage } from '../../../timelines/components/timeline/events'; import { Filter } from '../../../../../../../src/plugins/data/public'; -import { useUiSetting } from '../../lib/kibana'; import { EventsViewer } from './events_viewer'; -import { useFetchIndexPatterns } from '../../../detections/containers/detection_engine/rules/fetch_index_patterns'; import { InspectButtonContainer } from '../inspect'; import { useFullScreen } from '../../containers/use_full_screen'; +import { SourcererScopeName } from '../../store/sourcerer/model'; +import { useSourcererScope } from '../../containers/sourcerer'; const DEFAULT_EVENTS_VIEWER_HEIGHT = 652; @@ -35,10 +34,10 @@ const FullScreenContainer = styled.div<{ $isFullScreen: boolean }>` `; export interface OwnProps { - defaultIndices?: string[]; defaultModel: SubsetTimelineModel; end: string; id: string; + scopeId: SourcererScopeName; start: string; headerFilterGroup?: React.ReactNode; pageFilters?: Filter[]; @@ -52,7 +51,6 @@ const StatefulEventsViewerComponent: React.FC = ({ columns, dataProviders, deletedEventIds, - defaultIndices, deleteEventQuery, end, excludedRowRendererIds, @@ -67,6 +65,7 @@ const StatefulEventsViewerComponent: React.FC = ({ query, removeColumn, start, + scopeId, showCheckboxes, sort, updateItemsPerPage, @@ -75,13 +74,13 @@ const StatefulEventsViewerComponent: React.FC = ({ // If truthy, the graph viewer (Resolver) is showing graphEventId, }) => { - const [ - { docValueFields, browserFields, indexPatterns, isLoading: isLoadingIndexPattern }, - ] = useFetchIndexPatterns( - defaultIndices ?? useUiSetting(DEFAULT_INDEX_KEY), - 'events_viewer' - ); - + const { + browserFields, + docValueFields, + indexPattern, + selectedPatterns, + loading: isLoadingIndexPattern, + } = useSourcererScope(scopeId); const { globalFullScreen } = useFullScreen(); useEffect(() => { @@ -90,6 +89,7 @@ const StatefulEventsViewerComponent: React.FC = ({ id, columns, excludedRowRendererIds, + indexNames: selectedPatterns, sort, itemsPerPage, showCheckboxes, @@ -144,7 +144,8 @@ const StatefulEventsViewerComponent: React.FC = ({ isLoadingIndexPattern={isLoadingIndexPattern} filters={globalFilters} headerFilterGroup={headerFilterGroup} - indexPattern={indexPatterns} + indexNames={selectedPatterns} + indexPattern={indexPattern} isLive={isLive} itemsPerPage={itemsPerPage!} itemsPerPageOptions={itemsPerPageOptions!} @@ -222,8 +223,8 @@ export const StatefulEventsViewer = connector( StatefulEventsViewerComponent, (prevProps, nextProps) => prevProps.id === nextProps.id && + prevProps.scopeId === nextProps.scopeId && deepEqual(prevProps.columns, nextProps.columns) && - deepEqual(prevProps.defaultIndices, nextProps.defaultIndices) && deepEqual(prevProps.dataProviders, nextProps.dataProviders) && deepEqual(prevProps.excludedRowRendererIds, nextProps.excludedRowRendererIds) && prevProps.deletedEventIds === nextProps.deletedEventIds && diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index 691a7d99d9345..ed1c1c1cdad1f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -13,7 +13,7 @@ import { act } from 'react-dom/test-utils'; import { AddExceptionModal } from './'; import { useCurrentUser } from '../../../../common/lib/kibana'; import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock'; -import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; +import { useFetchIndex } from '../../../containers/source'; import { stubIndexPattern } from 'src/plugins/data/common/index_patterns/index_pattern.stub'; import { useAddOrUpdateException } from '../use_add_exception'; import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list'; @@ -28,6 +28,7 @@ import { ExceptionListItemSchema } from '../../../../../../lists/common'; jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index'); jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../containers/source'); jest.mock('../../../../detections/containers/detection_engine/rules'); jest.mock('../use_add_exception'); jest.mock('../use_fetch_or_create_rule_exception_list'); @@ -59,9 +60,9 @@ describe('When the add exception modal is opened', () => { loading: false, signalIndexName: 'mock-siem-signals-index', })); - (useFetchIndexPatterns as jest.Mock).mockImplementation(() => [ + (useFetchIndex as jest.Mock).mockImplementation(() => [ + false, { - isLoading: false, indexPatterns: stubIndexPattern, }, ]); @@ -77,9 +78,9 @@ describe('When the add exception modal is opened', () => { let wrapper: ReactWrapper; beforeEach(() => { // Mocks one of the hooks as loading - (useFetchIndexPatterns as jest.Mock).mockImplementation(() => [ + (useFetchIndex as jest.Mock).mockImplementation(() => [ + true, { - isLoading: true, indexPatterns: stubIndexPattern, }, ]); @@ -244,9 +245,9 @@ describe('When the add exception modal is opened', () => { }; beforeEach(() => { // Mocks the index patterns to contain the pre-populated endpoint fields so that the exception qualifies as bulk closable - (useFetchIndexPatterns as jest.Mock).mockImplementation(() => [ + (useFetchIndex as jest.Mock).mockImplementation(() => [ + false, { - isLoading: false, indexPatterns: { ...stubIndexPattern, fields: [ diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 721e53732c093..e945461f53e81 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -50,8 +50,8 @@ import { getMappedNonEcsValue, } from '../helpers'; import { ErrorInfo, ErrorCallout } from '../error_callout'; -import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; import { ExceptionsBuilderExceptionItem } from '../types'; +import { useFetchIndex } from '../../../containers/source'; export interface AddExceptionModalBaseProps { ruleName: string; @@ -122,14 +122,13 @@ export const AddExceptionModal = memo(function AddExceptionModal({ const [fetchOrCreateListError, setFetchOrCreateListError] = useState(null); const { addError, addSuccess, addWarning } = useAppToasts(); const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); - const [ - { isLoading: isSignalIndexPatternLoading, indexPatterns: signalIndexPatterns }, - ] = useFetchIndexPatterns(signalIndexName !== null ? [signalIndexName] : [], 'signals'); - - const [{ isLoading: isIndexPatternLoading, indexPatterns }] = useFetchIndexPatterns( - ruleIndices, - 'rules' + const memoSignalIndexName = useMemo(() => (signalIndexName !== null ? [signalIndexName] : []), [ + signalIndexName, + ]); + const [isSignalIndexPatternLoading, { indexPatterns: signalIndexPatterns }] = useFetchIndex( + memoSignalIndexName ); + const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(ruleIndices); const onError = useCallback( (error: Error): void => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx index c724e6a2c711f..d5d2091cc9bc8 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.test.tsx @@ -12,7 +12,7 @@ import { act } from 'react-dom/test-utils'; import { EditExceptionModal } from './'; import { useCurrentUser } from '../../../../common/lib/kibana'; -import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; +import { useFetchIndex } from '../../../containers/source'; import { stubIndexPattern, stubIndexPatternWithFields, @@ -26,6 +26,7 @@ import * as builder from '../builder'; jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../detections/containers/detection_engine/rules'); jest.mock('../use_add_exception'); +jest.mock('../../../containers/source'); jest.mock('../use_fetch_or_create_rule_exception_list'); jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index'); jest.mock('../builder'); @@ -50,9 +51,9 @@ describe('When the edit exception modal is opened', () => { { isLoading: false }, jest.fn(), ]); - (useFetchIndexPatterns as jest.Mock).mockImplementation(() => [ + (useFetchIndex as jest.Mock).mockImplementation(() => [ + false, { - isLoading: false, indexPatterns: stubIndexPatternWithFields, }, ]); @@ -67,9 +68,9 @@ describe('When the edit exception modal is opened', () => { describe('when the modal is loading', () => { let wrapper: ReactWrapper; beforeEach(() => { - (useFetchIndexPatterns as jest.Mock).mockImplementation(() => [ + (useFetchIndex as jest.Mock).mockImplementation(() => [ + true, { - isLoading: true, indexPatterns: stubIndexPattern, }, ]); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 5dbf319c3299d..128686428598c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -21,7 +21,8 @@ import { EuiText, EuiCallOut, } from '@elastic/eui'; -import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; + +import { useFetchIndex } from '../../../containers/source'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; import { @@ -108,15 +109,12 @@ export const EditExceptionModal = memo(function EditExceptionModal({ >([]); const { addError, addSuccess } = useAppToasts(); const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); - const [ - { isLoading: isSignalIndexPatternLoading, indexPatterns: signalIndexPatterns }, - ] = useFetchIndexPatterns(signalIndexName !== null ? [signalIndexName] : [], 'signals'); - - const [{ isLoading: isIndexPatternLoading, indexPatterns }] = useFetchIndexPatterns( - ruleIndices, - 'rules' + const [isSignalIndexPatternLoading, { indexPatterns: signalIndexPatterns }] = useFetchIndex( + signalIndexName !== null ? [signalIndexName] : [] ); + const [isIndexPatternLoading, { indexPatterns }] = useFetchIndex(ruleIndices); + const handleExceptionUpdateError = useCallback( (error: Error, statusCode: number | null, message: string | null) => { if (error.message.includes('Conflict')) { diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx index e05e3c2e9aeb1..5b4dd2e9728bb 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx @@ -18,7 +18,6 @@ import { getAppOverviewUrl } from '../link_to'; import { MlPopover } from '../ml_popover/ml_popover'; import { SiemNavigation } from '../navigation'; import * as i18n from './translations'; -import { useWithSource } from '../../containers/source'; import { useGetUrlSearch } from '../navigation/use_get_url_search'; import { useKibana } from '../../lib/kibana'; import { APP_ID, ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/constants'; @@ -58,11 +57,12 @@ interface HeaderGlobalProps { hideDetectionEngine?: boolean; } export const HeaderGlobal = React.memo(({ hideDetectionEngine = false }) => { - const { indicesExist } = useWithSource(); const { globalHeaderPortalNode } = useGlobalHeaderPortal(); const { globalFullScreen } = useFullScreen(); const search = useGetUrlSearch(navTabs.overview); - const { navigateToApp } = useKibana().services.application; + const { application, http } = useKibana().services; + const { navigateToApp } = application; + const basePath = http.basePath.get(); const goToOverview = useCallback( (ev) => { ev.preventDefault(); @@ -104,7 +104,7 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine - {indicesExist && window.location.pathname.includes(APP_DETECTIONS_PATH) && ( + {window.location.pathname.includes(APP_DETECTIONS_PATH) && ( @@ -113,7 +113,7 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine {i18n.BUTTON_ADD_DATA} diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap index a100f5e4f93b4..a2a36b3fe1d3b 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap @@ -36,5 +36,8 @@ exports[`HeaderPage it renders 1`] = `

    +

`; diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx index 62880e7510cd2..0cb721bb5382f 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx @@ -15,6 +15,8 @@ import { Title } from './title'; import { DraggableArguments, BadgeOptions, TitleProp } from './types'; import { useFormatUrl } from '../link_to'; import { SecurityPageName } from '../../../app/types'; +import { Sourcerer } from '../sourcerer'; +import { SourcererScopeName } from '../../store/sourcerer/model'; interface HeaderProps { border?: boolean; @@ -72,6 +74,7 @@ export interface HeaderPageProps extends HeaderProps { badgeOptions?: BadgeOptions; children?: React.ReactNode; draggableArguments?: DraggableArguments; + hideSourcerer?: boolean; subtitle?: SubtitleProps['items']; subtitle2?: SubtitleProps['items']; title: TitleProp; @@ -84,6 +87,7 @@ const HeaderPageComponent: React.FC = ({ border, children, draggableArguments, + hideSourcerer = false, isLoading, subtitle, subtitle2, @@ -138,6 +142,7 @@ const HeaderPageComponent: React.FC = ({ )} + {!hideSourcerer && } ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/last_event_time/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/last_event_time/index.test.tsx index 9473ba67a1c4f..c2800b0705b43 100644 --- a/x-pack/plugins/security_solution/public/common/components/last_event_time/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/last_event_time/index.test.tsx @@ -37,7 +37,7 @@ describe('Last Event Time Stat', () => { ]); const wrapper = mount( - + ); expect(wrapper.html()).toBe( @@ -54,7 +54,7 @@ describe('Last Event Time Stat', () => { ]); const wrapper = mount( - + ); expect(wrapper.html()).toBe('Last event: 12 minutes ago'); @@ -69,7 +69,7 @@ describe('Last Event Time Stat', () => { ]); const wrapper = mount( - + ); @@ -85,7 +85,7 @@ describe('Last Event Time Stat', () => { ]); const wrapper = mount( - + ); expect(wrapper.html()).toContain(getEmptyValue()); diff --git a/x-pack/plugins/security_solution/public/common/components/last_event_time/index.tsx b/x-pack/plugins/security_solution/public/common/components/last_event_time/index.tsx index e9e8e7a03017c..d508040f84239 100644 --- a/x-pack/plugins/security_solution/public/common/components/last_event_time/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/last_event_time/index.tsx @@ -8,58 +8,65 @@ import { EuiIcon, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { memo } from 'react'; +import { DocValueFields } from '../../../../common/search_strategy'; import { LastEventIndexKey } from '../../../graphql/types'; import { useTimelineLastEventTime } from '../../containers/events/last_event_time'; import { getEmptyTagValue } from '../empty_value'; import { FormattedRelativePreferenceDate } from '../formatted_date'; export interface LastEventTimeProps { + docValueFields: DocValueFields[]; hostName?: string; indexKey: LastEventIndexKey; ip?: string; + indexNames: string[]; } -export const LastEventTime = memo(({ hostName, indexKey, ip }) => { - const [loading, { lastSeen, errorMessage }] = useTimelineLastEventTime({ - indexKey, - details: { - hostName, - ip, - }, - }); +export const LastEventTime = memo( + ({ docValueFields, hostName, indexKey, ip, indexNames }) => { + const [loading, { lastSeen, errorMessage }] = useTimelineLastEventTime({ + docValueFields, + indexKey, + indexNames, + details: { + hostName, + ip, + }, + }); + + if (errorMessage != null) { + return ( + + + + ); + } - if (errorMessage != null) { return ( - - - + <> + {loading && } + {!loading && lastSeen != null && new Date(lastSeen).toString() === 'Invalid Date' + ? lastSeen + : !loading && + lastSeen != null && ( + , + }} + /> + )} + {!loading && lastSeen == null && getEmptyTagValue()} + ); } - - return ( - <> - {loading && } - {!loading && lastSeen != null && new Date(lastSeen).toString() === 'Invalid Date' - ? lastSeen - : !loading && - lastSeen != null && ( - , - }} - /> - )} - {!loading && lastSeen == null && getEmptyTagValue()} - - ); -}); +); LastEventTime.displayName = 'LastEventTime'; diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx index 7286c6b743692..99dc8a802b33d 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx @@ -47,6 +47,7 @@ describe('Matrix Histogram Component', () => { errorMessage: 'error', histogramType: MatrixHistogramType.alerts, id: 'mockId', + indexNames: [], isInspected: false, isPtrIncluded: false, setQuery: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index 485ca4c93133a..e7d7e60a3c408 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -37,7 +37,6 @@ export type MatrixHistogramComponentProps = MatrixHistogramProps & hideHistogramIfEmpty?: boolean; histogramType: MatrixHistogramType; id: string; - indexToAdd?: string[] | null; legendPosition?: Position; mapping?: MatrixHistogramMappingTypes; showSpacer?: boolean; @@ -72,7 +71,7 @@ export const MatrixHistogramComponent: React.FC = histogramType, hideHistogramIfEmpty = false, id, - indexToAdd, + indexNames, legendPosition, mapping, panelHeight = DEFAULT_PANEL_HEIGHT, @@ -136,7 +135,7 @@ export const MatrixHistogramComponent: React.FC = errorMessage, filterQuery, histogramType, - indexToAdd, + indexNames, startDate, stackByField: selectedStackByOption.value, }); diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index fc1df4d8ca85f..9a892110bde43 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -59,6 +59,7 @@ interface MatrixHistogramBasicProps { export interface MatrixHistogramQueryProps { endDate: string; errorMessage: string; + indexNames: string[]; filterQuery?: ESQuery | string | undefined; setAbsoluteRangeDatePicker?: ActionCreator<{ id: InputsModelId; @@ -68,7 +69,6 @@ export interface MatrixHistogramQueryProps { setAbsoluteRangeDatePickerTarget?: InputsModelId; stackByField: string; startDate: string; - indexToAdd?: string[] | null; histogramType: MatrixHistogramType; } diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts index 89aa77106933e..da5099f61e9b2 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts @@ -105,6 +105,7 @@ const getMockObject = ( }, }, }, + sourcerer: {}, }); const getUrlForAppMock = (appId: string, options?: { path?: string; absolute?: boolean }) => @@ -130,7 +131,7 @@ describe('Navigation Breadcrumbs', () => { }, { href: - "securitySolution:hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + "securitySolution:hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", text: 'Hosts', }, { @@ -150,7 +151,7 @@ describe('Navigation Breadcrumbs', () => { { text: 'Network', href: - "securitySolution:network?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + "securitySolution:network?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: 'Flows', @@ -169,7 +170,7 @@ describe('Navigation Breadcrumbs', () => { { text: 'Timelines', href: - "securitySolution:timelines?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + "securitySolution:timelines?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, ]); }); @@ -184,12 +185,12 @@ describe('Navigation Breadcrumbs', () => { { text: 'Hosts', href: - "securitySolution:hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + "securitySolution:hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: 'siem-kibana', href: - "securitySolution:hosts/siem-kibana?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + "securitySolution:hosts/siem-kibana?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: 'Authentications', href: '' }, ]); @@ -205,11 +206,11 @@ describe('Navigation Breadcrumbs', () => { { text: 'Network', href: - "securitySolution:network?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + "securitySolution:network?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: ipv4, - href: `securitySolution:network/ip/${ipv4}/source?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, + href: `securitySolution:network/ip/${ipv4}/source?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, }, { text: 'Flows', href: '' }, ]); @@ -225,11 +226,11 @@ describe('Navigation Breadcrumbs', () => { { text: 'Network', href: - "securitySolution:network?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + "securitySolution:network?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: ipv6, - href: `securitySolution:network/ip/${ipv6Encoded}/source?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, + href: `securitySolution:network/ip/${ipv6Encoded}/source?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, }, { text: 'Flows', href: '' }, ]); @@ -245,7 +246,7 @@ describe('Navigation Breadcrumbs', () => { { text: 'Detections', href: - "securitySolution:detections?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + "securitySolution:detections?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, ]); }); @@ -259,7 +260,7 @@ describe('Navigation Breadcrumbs', () => { { text: 'Cases', href: - "securitySolution:case?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + "securitySolution:case?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, ]); }); @@ -280,11 +281,11 @@ describe('Navigation Breadcrumbs', () => { { text: 'Cases', href: - "securitySolution:case?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + "securitySolution:case?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: sampleCase.name, - href: `securitySolution:case/${sampleCase.id}?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, + href: `securitySolution:case/${sampleCase.id}?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, }, ]); }); @@ -311,12 +312,12 @@ describe('Navigation Breadcrumbs', () => { { text: 'Hosts', href: - "securitySolution:hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + "securitySolution:hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: 'siem-kibana', href: - "securitySolution:hosts/siem-kibana?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + "securitySolution:hosts/siem-kibana?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", }, { text: 'Authentications', href: '' }, ]); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts b/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts index 8f5a3ac63fa1a..ed71f55fd0161 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts @@ -19,12 +19,19 @@ import { import { Query, Filter } from '../../../../../../../src/plugins/data/public'; import { SearchNavTab } from './types'; +import { SourcererScopePatterns } from '../../store/sourcerer/model'; export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => { if (tab && tab.urlKey != null && URL_STATE_KEYS[tab.urlKey] != null) { return URL_STATE_KEYS[tab.urlKey].reduce( (myLocation: Location, urlKey: KeyUrlState) => { - let urlStateToReplace: UrlInputsModel | Query | Filter[] | TimelineUrl | string = ''; + let urlStateToReplace: + | Filter[] + | Query + | SourcererScopePatterns + | TimelineUrl + | UrlInputsModel + | string = ''; if (urlKey === CONSTANTS.appQuery && urlState.query != null) { if (urlState.query.query === '') { @@ -40,6 +47,8 @@ export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => { } } else if (urlKey === CONSTANTS.timerange) { urlStateToReplace = urlState[CONSTANTS.timerange]; + } else if (urlKey === CONSTANTS.sourcerer) { + urlStateToReplace = urlState[CONSTANTS.sourcerer]; } else if (urlKey === CONSTANTS.timeline && urlState[CONSTANTS.timeline] != null) { const timeline = urlState[CONSTANTS.timeline]; if (timeline.id === '') { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx index 16cb19f5a0c14..102ed7851e57d 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx @@ -78,6 +78,7 @@ describe('SIEM Navigation', () => { }, [CONSTANTS.appQuery]: { query: '', language: 'kuery' }, [CONSTANTS.filters]: [], + [CONSTANTS.sourcerer]: {}, [CONSTANTS.timeline]: { id: '', isOpen: false, @@ -145,6 +146,7 @@ describe('SIEM Navigation', () => { pageName: 'hosts', pathName: '/', search: '', + sourcerer: {}, state: undefined, tabName: 'authentications', query: { query: '', language: 'kuery' }, @@ -252,6 +254,7 @@ describe('SIEM Navigation', () => { query: { language: 'kuery', query: '' }, savedQuery: undefined, search: '', + sourcerer: {}, state: undefined, tabName: 'authentications', timeline: { id: '', isOpen: false, graphEventId: '' }, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx index 5ee35e7da0f3e..b149488ff38a7 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx @@ -40,19 +40,20 @@ export const SiemNavigationComponent: React.FC< if (pathName || pageName) { setBreadcrumbs( { - query: urlState.query, detailName, filters: urlState.filters, + flowTarget, navTabs, pageName, pathName, + query: urlState.query, savedQuery: urlState.savedQuery, search, + sourcerer: urlState.sourcerer, + state, tabName, - flowTarget, - timerange: urlState.timerange, timeline: urlState.timeline, - state, + timerange: urlState.timerange, }, chrome, getUrlForApp @@ -69,6 +70,7 @@ export const SiemNavigationComponent: React.FC< navTabs={navTabs} pageName={pageName} pathName={pathName} + sourcerer={urlState.sourcerer} savedQuery={urlState.savedQuery} tabName={tabName} timeline={urlState.timeline} diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx index b25cf3779801b..5c69edbabdc66 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx @@ -68,6 +68,7 @@ describe('Tab Navigation', () => { }, [CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' }, [CONSTANTS.filters]: [], + [CONSTANTS.sourcerer]: {}, [CONSTANTS.timeline]: { id: '', isOpen: false, @@ -126,6 +127,7 @@ describe('Tab Navigation', () => { }, [CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' }, [CONSTANTS.filters]: [], + [CONSTANTS.sourcerer]: {}, [CONSTANTS.timeline]: { id: '', isOpen: false, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx index 217ad0e58570f..3eb66b5591b85 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx @@ -94,10 +94,17 @@ export const TabNavigationComponent = (props: TabNavigationProps) => { () => Object.values(navTabs).map((tab) => { const isSelected = selectedTabId === tab.id; - const { query, filters, savedQuery, timerange, timeline } = props; - const search = getSearch(tab, { query, filters, savedQuery, timerange, timeline }); + const { filters, query, savedQuery, sourcerer, timeline, timerange } = props; + const search = getSearch(tab, { + filters, + query, + savedQuery, + sourcerer, + timeline, + timerange, + }); const hrefWithSearch = - tab.href + getSearch(tab, { query, filters, savedQuery, timerange, timeline }); + tab.href + getSearch(tab, { filters, query, savedQuery, sourcerer, timeline, timerange }); return ( { - const original = jest.requireActual('../../containers/sourcerer'); +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); return { ...original, - useManageSource: () => mockManageSource, + useDispatch: () => mockDispatch, }; }); const mockOptions = [ - { label: 'auditbeat-*', key: 'auditbeat-*-0', value: 'auditbeat-*', checked: 'on' }, - { label: 'endgame-*', key: 'endgame-*-1', value: 'endgame-*', checked: 'on' }, - { label: 'filebeat-*', key: 'filebeat-*-2', value: 'filebeat-*', checked: 'on' }, - { label: 'logs-*', key: 'logs-*-3', value: 'logs-*', checked: 'on' }, - { label: 'packetbeat-*', key: 'packetbeat-*-4', value: 'packetbeat-*', checked: undefined }, - { label: 'winlogbeat-*', key: 'winlogbeat-*-5', value: 'winlogbeat-*', checked: 'on' }, - { - label: 'apm-*-transaction*', - key: 'apm-*-transaction*-0', - value: 'apm-*-transaction*', - disabled: true, - checked: undefined, - }, - { - label: 'blobbeat-*', - key: 'blobbeat-*-1', - value: 'blobbeat-*', - disabled: true, - checked: undefined, - }, + { label: 'apm-*-transaction*', value: 'apm-*-transaction*' }, + { label: 'auditbeat-*', value: 'auditbeat-*' }, + { label: 'endgame-*', value: 'endgame-*' }, + { label: 'filebeat-*', value: 'filebeat-*' }, + { label: 'logs-*', value: 'logs-*' }, + { label: 'packetbeat-*', value: 'packetbeat-*' }, + { label: 'winlogbeat-*', value: 'winlogbeat-*' }, ]; +const defaultProps = { + scope: sourcererModel.SourcererScopeName.default, +}; describe('Sourcerer component', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + const state: State = mockGlobalState; + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore( + state, + SUB_PLUGINS_REDUCER, + apolloClientObservable, + kibanaObservable, + storage + ); + + beforeEach(() => { + store = createStore( + state, + SUB_PLUGINS_REDUCER, + apolloClientObservable, + kibanaObservable, + storage + ); + }); + // Using props callback instead of simulating clicks, // because EuiSelectable uses a virtualized list, which isn't easily testable via test subjects - it('Mounts with correct options selected and disabled', () => { - const wrapper = mount(); + it('Mounts with all options selected', () => { + const wrapper = mount( + + + + ); wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); - expect( - wrapper.find(`[data-test-subj="indexPattern-switcher"]`).first().prop('options') + wrapper.find(`[data-test-subj="indexPattern-switcher"]`).first().prop('selectedOptions') ).toEqual(mockOptions); }); - it('onChange calls updateSourceGroupIndicies', () => { - const wrapper = mount(); - wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); - - const switcherOnChange = wrapper - .find(`[data-test-subj="indexPattern-switcher"]`) - .first() - .prop('onChange'); - // @ts-ignore - switcherOnChange([mockOptions[0], mockOptions[1]]); - expect(updateSourceGroupIndicies).toHaveBeenCalledWith(SecurityPageName.default, [ - mockOptions[0].value, - mockOptions[1].value, - ]); - }); - it('Disabled options have icon tooltip', () => { - const wrapper = mount(); - wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); - // @ts-ignore - const Rendered = wrapper - .find(`[data-test-subj="indexPattern-switcher"]`) - .first() - .prop('renderOption')( - { - label: 'blobbeat-*', - key: 'blobbeat-*-1', - value: 'blobbeat-*', - disabled: true, - checked: undefined, + it('Mounts with some options selected', () => { + const state2 = { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + [SourcererScopeName.default]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], + loading: false, + selectedPatterns: [DEFAULT_INDEX_PATTERN[0]], + }, + }, }, - '' + }; + + store = createStore( + state2, + SUB_PLUGINS_REDUCER, + apolloClientObservable, + kibanaObservable, + storage + ); + const wrapper = mount( + + + ); - expect(Rendered.props.children[1].props.content).toEqual(i18n.DISABLED_INDEX_PATTERNS); + wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); + expect( + wrapper.find(`[data-test-subj="indexPattern-switcher"]`).first().prop('selectedOptions') + ).toEqual([mockOptions[0]]); }); - - it('Button links to index path', () => { - const wrapper = mount(); + it('onChange calls updateSourcererScopeIndices', async () => { + const wrapper = mount( + + + + ); + expect(true).toBeTruthy(); wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); - expect(wrapper.find(`[data-test-subj="add-index"]`).first().prop('href')).toEqual( - ADD_INDEX_PATH + await act(async () => { + ((wrapper.find(EuiComboBox).props() as unknown) as { + onChange: (a: EuiComboBoxOptionOption[]) => void; + }).onChange([mockOptions[0], mockOptions[1]]); + await waitFor(() => { + wrapper.update(); + }); + }); + wrapper.find(`[data-test-subj="add-index"]`).first().simulate('click'); + + expect(mockDispatch).toHaveBeenCalledWith( + sourcererActions.setSelectedIndexPatterns({ + id: SourcererScopeName.default, + selectedPatterns: [mockOptions[0].value, mockOptions[1].value], + }) ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx index 6275ce19c3608..7a74f5bf2247f 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx @@ -4,50 +4,122 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo, useState } from 'react'; import { EuiButton, EuiButtonEmpty, - EuiHighlight, - EuiIconTip, + EuiComboBox, + EuiComboBoxOptionOption, + EuiIcon, + EuiFlexGroup, + EuiFlexItem, EuiPopover, - EuiPopoverFooter, EuiPopoverTitle, - EuiSelectable, + EuiSpacer, + EuiText, + EuiToolTip, } from '@elastic/eui'; -import { EuiSelectableOption } from '@elastic/eui/src/components/selectable/selectable_option'; -import { useManageSource } from '../../containers/sourcerer'; +import deepEqual from 'fast-deep-equal'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; + import * as i18n from './translations'; import { SOURCERER_FEATURE_FLAG_ON } from '../../containers/sourcerer/constants'; -import { ADD_INDEX_PATH } from '../../../../common/constants'; - -export const MaybeSourcerer = React.memo(() => { - const { - activeSourceGroupId, - availableIndexPatterns, - getManageSourceGroupById, - isIndexPatternsLoading, - updateSourceGroupIndicies, - } = useManageSource(); - const { defaultPatterns, indexPatterns: selectedOptions, loading: loadingIndices } = useMemo( - () => getManageSourceGroupById(activeSourceGroupId), - [getManageSourceGroupById, activeSourceGroupId] +import { sourcererActions, sourcererModel } from '../../store/sourcerer'; +import { State } from '../../store'; +import { getSourcererScopeSelector, SourcererScopeSelector } from './selectors'; + +const PopoverContent = styled.div` + width: 600px; +`; + +const ResetButton = styled(EuiButtonEmpty)` + width: fit-content; +`; +interface SourcererComponentProps { + scope: sourcererModel.SourcererScopeName; +} + +export const SourcererComponent = React.memo(({ scope: scopeId }) => { + const dispatch = useDispatch(); + const sourcererScopeSelector = useMemo(getSourcererScopeSelector, []); + const { configIndexPatterns, kibanaIndexPatterns, sourcererScope } = useSelector< + State, + SourcererScopeSelector + >((state) => sourcererScopeSelector(state, scopeId), deepEqual); + const { selectedPatterns, loading } = sourcererScope; + const [isPopoverOpen, setPopoverIsOpen] = useState(false); + const [selectedOptions, setSelectedOptions] = useState>>( + selectedPatterns.map((indexSelected) => ({ + label: indexSelected, + value: indexSelected, + })) ); - const loading = useMemo(() => loadingIndices || isIndexPatternsLoading, [ - isIndexPatternsLoading, - loadingIndices, - ]); + const setPopoverIsOpenCb = useCallback(() => setPopoverIsOpen((prevState) => !prevState), []); const onChangeIndexPattern = useCallback( - (newIndexPatterns: string[]) => { - updateSourceGroupIndicies(activeSourceGroupId, newIndexPatterns); + (newSelectedPatterns: string[]) => { + dispatch( + sourcererActions.setSelectedIndexPatterns({ + id: scopeId, + selectedPatterns: newSelectedPatterns, + }) + ); }, - [activeSourceGroupId, updateSourceGroupIndicies] + [dispatch, scopeId] + ); + + const renderOption = useCallback( + (option) => { + const { value } = option; + if (kibanaIndexPatterns.some((kip) => kip.title === value)) { + return ( + <> + {value} + + ); + } + return <>{value}; + }, + [kibanaIndexPatterns] + ); + + const onChangeCombo = useCallback((newSelectedOptions) => { + setSelectedOptions(newSelectedOptions); + }, []); + + const resetDataSources = useCallback(() => { + setSelectedOptions( + configIndexPatterns.map((indexSelected) => ({ + label: indexSelected, + value: indexSelected, + })) + ); + }, [configIndexPatterns]); + + const handleSaveIndices = useCallback(() => { + onChangeIndexPattern(selectedOptions.map((so) => so.label)); + setPopoverIsOpen(false); + }, [onChangeIndexPattern, selectedOptions]); + + const handleClosePopOver = useCallback(() => { + setPopoverIsOpen(false); + }, []); + + const indexesPatternOptions = useMemo( + () => + [...configIndexPatterns, ...kibanaIndexPatterns.map((kip) => kip.title)].reduce< + Array> + >((acc, index) => { + if (index != null && !acc.some((o) => o.label.includes(index))) { + return [...acc, { label: index, value: index }]; + } + return acc; + }, []), + [configIndexPatterns, kibanaIndexPatterns] ); - const [isPopoverOpen, setPopoverIsOpen] = useState(false); - const setPopoverIsOpenCb = useCallback(() => setPopoverIsOpen((prevState) => !prevState), []); const trigger = useMemo( () => ( { data-test-subj="sourcerer-trigger" flush="left" iconSide="right" - iconType="indexSettings" + iconType="arrowDown" + isLoading={loading} onClick={setPopoverIsOpenCb} size="l" title={i18n.SOURCERER} @@ -63,99 +136,91 @@ export const MaybeSourcerer = React.memo(() => { {i18n.SOURCERER} ), - [setPopoverIsOpenCb] - ); - const options: EuiSelectableOption[] = useMemo( - () => - availableIndexPatterns.map((title, id) => ({ - label: title, - key: `${title}-${id}`, - value: title, - checked: selectedOptions.includes(title) ? 'on' : undefined, - })), - [availableIndexPatterns, selectedOptions] + [setPopoverIsOpenCb, loading] ); - const unSelectableOptions: EuiSelectableOption[] = useMemo( - () => - defaultPatterns - .filter((title) => !availableIndexPatterns.includes(title)) - .map((title, id) => ({ - label: title, - key: `${title}-${id}`, - value: title, - disabled: true, - checked: undefined, - })), - [availableIndexPatterns, defaultPatterns] - ); - const renderOption = useCallback( - (option, searchValue) => ( - <> - {option.label} - {option.disabled ? ( - - ) : null} - + + const comboBox = useMemo( + () => ( + ), - [] + [indexesPatternOptions, onChangeCombo, renderOption, selectedOptions] ); - const onChange = useCallback( - (choices: EuiSelectableOption[]) => { - const choice = choices.reduce( - (acc, { checked, label }) => (checked === 'on' ? [...acc, label] : acc), - [] - ); - onChangeIndexPattern(choice); - }, - [onChangeIndexPattern] + + useEffect(() => { + const newSelecteOptions = selectedPatterns.map((indexSelected) => ({ + label: indexSelected, + value: indexSelected, + })); + setSelectedOptions((prevSelectedOptions) => { + if (!deepEqual(newSelecteOptions, prevSelectedOptions)) { + return newSelecteOptions; + } + return prevSelectedOptions; + }); + }, [selectedPatterns]); + + const tooltipContent = useMemo( + () => (isPopoverOpen ? null : sourcererScope.selectedPatterns.sort().join(', ')), + [isPopoverOpen, sourcererScope.selectedPatterns] ); - const allOptions = useMemo(() => [...options, ...unSelectableOptions], [ - options, - unSelectableOptions, - ]); + return ( - setPopoverIsOpen(false)} - display="block" - panelPaddingSize="s" - ownFocus - > -
- - <> - {i18n.CHANGE_INDEX_PATTERNS} - - - - - {(list, search) => ( - <> - {search} - {list} - - )} - - - - {i18n.ADD_INDEX_PATTERNS} - - -
-
+ + + + + <>{i18n.SELECT_INDEX_PATTERNS} + + + {i18n.INDEX_PATTERNS_SELECTION_LABEL} + + {comboBox} + + + + + {i18n.INDEX_PATTERNS_RESET} + + + + + {i18n.SAVE_INDEX_PATTERNS} + + + + + + ); }); -MaybeSourcerer.displayName = 'Sourcerer'; +SourcererComponent.displayName = 'Sourcerer'; -export const Sourcerer = SOURCERER_FEATURE_FLAG_ON ? MaybeSourcerer : () => null; +export const Sourcerer = SOURCERER_FEATURE_FLAG_ON ? SourcererComponent : () => null; diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/selectors.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/selectors.tsx new file mode 100644 index 0000000000000..6bbe24e921880 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/selectors.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { State } from '../../store'; +import { sourcererSelectors } from '../../store/sourcerer'; +import { KibanaIndexPatterns, ManageScope, SourcererScopeName } from '../../store/sourcerer/model'; + +export interface SourcererScopeSelector { + configIndexPatterns: string[]; + kibanaIndexPatterns: KibanaIndexPatterns; + sourcererScope: ManageScope; +} + +export const getSourcererScopeSelector = () => { + const getKibanaIndexPatternsSelector = sourcererSelectors.kibanaIndexPatternsSelector(); + const getScopesSelector = sourcererSelectors.scopesSelector(); + const getConfigIndexPatternsSelector = sourcererSelectors.configIndexPatternsSelector(); + + const mapStateToProps = (state: State, scopeId: SourcererScopeName): SourcererScopeSelector => { + const kibanaIndexPatterns = getKibanaIndexPatternsSelector(state); + const scope = getScopesSelector(state)[scopeId]; + const configIndexPatterns = getConfigIndexPatternsSelector(state); + + return { + kibanaIndexPatterns, + configIndexPatterns, + sourcererScope: scope, + }; + }; + + return mapStateToProps; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts b/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts index 71b1734dad6a6..473eb43d5c4fe 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts @@ -6,23 +6,26 @@ import { i18n } from '@kbn/i18n'; -export const SOURCERER = i18n.translate('xpack.securitySolution.indexPatterns.sourcerer', { - defaultMessage: 'Sourcerer', +export const SOURCERER = i18n.translate('xpack.securitySolution.indexPatterns.dataSourcesLabel', { + defaultMessage: 'Data sources', }); -export const CHANGE_INDEX_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.help', { - defaultMessage: 'Change index patterns', +export const ALL_DEFAULT = i18n.translate('xpack.securitySolution.indexPatterns.allDefault', { + defaultMessage: 'All default', }); -export const ADD_INDEX_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.add', { - defaultMessage: 'Configure Kibana index patterns', +export const SELECT_INDEX_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.help', { + defaultMessage: 'Data sources selection', }); -export const CONFIGURE_INDEX_PATTERNS = i18n.translate( - 'xpack.securitySolution.indexPatterns.configure', +export const SAVE_INDEX_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.save', { + defaultMessage: 'Save', +}); + +export const INDEX_PATTERNS_SELECTION_LABEL = i18n.translate( + 'xpack.securitySolution.indexPatterns.selectionLabel', { - defaultMessage: - 'Configure additional Kibana index patterns to see them become available in the Security Solution', + defaultMessage: 'Choose the source of the data on this page', } ); @@ -33,3 +36,17 @@ export const DISABLED_INDEX_PATTERNS = i18n.translate( 'Disabled index patterns are recommended on this page, but first need to be configured in your Kibana index pattern settings', } ); + +export const INDEX_PATTERNS_RESET = i18n.translate( + 'xpack.securitySolution.indexPatterns.resetButton', + { + defaultMessage: 'Reset', + } +); + +export const PICK_INDEX_PATTERNS = i18n.translate( + 'xpack.securitySolution.indexPatterns.pickIndexPatternsCombo', + { + defaultMessage: 'Pick index patterns', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/helpers.ts b/x-pack/plugins/security_solution/public/common/components/top_n/helpers.ts index b654eaf17b47b..79cbd87cda201 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/top_n/helpers.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EventType } from '../../../timelines/store/timeline/model'; +import { TimelineEventsType } from '../../../../common/types/timeline'; import * as i18n from './translations'; export interface TopNOption { inputDisplay: string; - value: EventType; + value: TimelineEventsType; 'data-test-subj': string; } @@ -52,8 +52,8 @@ export const defaultOptions = [...rawEvents, ...alertEvents]; * is always in sync with the `EventType` chosen by the user in * the active timeline. */ -export const getOptions = (activeTimelineEventType?: EventType): TopNOption[] => { - switch (activeTimelineEventType) { +export const getOptions = (activeTimelineEventsType?: TimelineEventsType): TopNOption[] => { + switch (activeTimelineEventsType) { case 'all': return allEvents; case 'raw': diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx index 31318122eb564..594bffbd4ff63 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx @@ -168,6 +168,17 @@ const store = createStore( storage ); +let testProps = { + browserFields: mockBrowserFields, + field, + indexNames: [], + indexPattern: mockIndexPattern, + timelineId: TimelineId.hostsPageExternalAlerts, + toggleTopN: jest.fn(), + onFilterAdded: jest.fn(), + value, +}; + describe('StatefulTopN', () => { // Suppress warnings about "react-beautiful-dnd" /* eslint-disable no-console */ @@ -189,16 +200,7 @@ describe('StatefulTopN', () => { wrapper = mount( - + ); @@ -277,19 +279,14 @@ describe('StatefulTopN', () => { filterManager, }, }; + testProps = { + ...testProps, + timelineId: TimelineId.active, + }; wrapper = mount( - + ); @@ -345,37 +342,33 @@ describe('StatefulTopN', () => { expect(props.to).toEqual('2020-04-15T03:46:09.047Z'); }); }); + describe('rendering in a NON-active timeline context', () => { + test(`defaults to the 'Alert events' option when rendering in a NON-active timeline context (e.g. the Alerts table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'alerts'`, () => { + const filterManager = new FilterManager(mockUiSettingsForFilterManager); - test(`defaults to the 'Alert events' option when rendering in a NON-active timeline context (e.g. the Alerts table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'alerts'`, () => { - const filterManager = new FilterManager(mockUiSettingsForFilterManager); + const manageTimelineForTesting = { + [TimelineId.active]: { + ...getTimelineDefaults(TimelineId.active), + filterManager, + documentType: 'alerts', + }, + }; - const manageTimelineForTesting = { - [TimelineId.active]: { - ...getTimelineDefaults(TimelineId.active), - filterManager, - documentType: 'alerts', - }, - }; - - const wrapper = mount( - - - - - - ); - - const props = wrapper.find('[data-test-subj="top-n"]').first().props() as Props; - - expect(props.defaultView).toEqual('alert'); + testProps = { + ...testProps, + timelineId: TimelineId.detectionsPage, + }; + const wrapper = mount( + + + + + + ); + + const props = wrapper.find('[data-test-subj="top-n"]').first().props() as Props; + + expect(props.defaultView).toEqual('alert'); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx index d71242329bcda..9c81cb57335a5 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx @@ -74,7 +74,7 @@ interface OwnProps { browserFields: BrowserFields; field: string; indexPattern: IIndexPattern; - indexToAdd: string[] | null; + indexNames: string[]; timelineId?: string; toggleTopN: () => void; onFilterAdded?: () => void; @@ -93,7 +93,7 @@ const StatefulTopNComponent: React.FC = ({ dataProviders, field, indexPattern, - indexToAdd, + indexNames, globalFilters = EMPTY_FILTERS, globalQuery = EMPTY_QUERY, kqlMode, @@ -109,7 +109,6 @@ const StatefulTopNComponent: React.FC = ({ const options = getOptions( timelineId === TimelineId.active ? activeTimelineEventType : undefined ); - return ( = ({ filters={timelineId === TimelineId.active ? EMPTY_FILTERS : globalFilters} from={timelineId === TimelineId.active ? activeTimelineFrom : from} indexPattern={indexPattern} - indexToAdd={indexToAdd} + indexNames={indexNames} options={options} query={timelineId === TimelineId.active ? EMPTY_QUERY : globalQuery} setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx index 667d1816e8f07..829f918ddfe1b 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx @@ -13,6 +13,8 @@ import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { allEvents, defaultOptions } from './helpers'; import { TopN } from './top_n'; +import { TimelineEventsType } from '../../../../common/types/timeline'; +import { InputsModelId } from '../../store/inputs/constants'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -103,29 +105,34 @@ describe('TopN', () => { const query = { query: '', language: 'kuery' }; + const toggleTopN = jest.fn(); + const eventTypes: { [id: string]: TimelineEventsType } = { + raw: 'raw', + alert: 'alert', + all: 'all', + }; + let testProps = { + defaultView: eventTypes.raw, + field, + filters: [], + from: '2020-04-14T00:31:47.695Z', + indexNames: [], + indexPattern: mockIndexPattern, + options: defaultOptions, + query, + setAbsoluteRangeDatePicker, + setAbsoluteRangeDatePickerTarget: 'global' as InputsModelId, + setQuery: jest.fn(), + to: '2020-04-15T00:31:47.695Z', + toggleTopN, + value, + }; describe('common functionality', () => { - let toggleTopN: () => void; let wrapper: ReactWrapper; - beforeEach(() => { - toggleTopN = jest.fn(); wrapper = mount( - + ); }); @@ -143,28 +150,12 @@ describe('TopN', () => { }); describe('events view', () => { - let toggleTopN: () => void; let wrapper: ReactWrapper; beforeEach(() => { - toggleTopN = jest.fn(); wrapper = mount( - + ); }); @@ -181,37 +172,25 @@ describe('TopN', () => { }); describe('alerts view', () => { - let toggleTopN: () => void; let wrapper: ReactWrapper; beforeEach(() => { - toggleTopN = jest.fn(); + testProps = { + ...testProps, + defaultView: eventTypes.alert, + }; wrapper = mount( - + ); }); - test(`it renders SignalsByCategory when defaultView is 'signal'`, () => { + test(`it renders SignalsByCategory when defaultView is 'alert'`, () => { expect(wrapper.find('[data-test-subj="alerts-histogram-panel"]').exists()).toBe(true); }); - test(`it does NOT render EventsByDataset when defaultView is 'signal'`, () => { + test(`it does NOT render EventsByDataset when defaultView is 'alert'`, () => { expect( wrapper.find('[data-test-subj="eventsByDatasetOverview-uuid.v4()Panel"]').exists() ).toBe(false); @@ -222,24 +201,14 @@ describe('TopN', () => { let wrapper: ReactWrapper; beforeEach(() => { + testProps = { + ...testProps, + defaultView: eventTypes.all, + options: allEvents, + }; wrapper = mount( - + ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx index 064241a7216f4..4f0a71dcc3ebb 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx @@ -14,7 +14,7 @@ import { EventsByDataset } from '../../../overview/components/events_by_dataset' import { SignalsByCategory } from '../../../overview/components/signals_by_category'; import { Filter, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public'; import { InputsModelId } from '../../store/inputs/constants'; -import { EventType } from '../../../timelines/store/timeline/model'; +import { TimelineEventsType } from '../../../../common/types/timeline'; import { TopNOption } from './helpers'; import * as i18n from './translations'; @@ -45,11 +45,11 @@ const TopNContent = styled.div` export interface Props extends Pick { combinedQueries?: string; - defaultView: EventType; + defaultView: TimelineEventsType; field: string; filters: Filter[]; indexPattern: IIndexPattern; - indexToAdd?: string[] | null; + indexNames: string[]; options: TopNOption[]; query: Query; setAbsoluteRangeDatePicker: ActionCreator<{ @@ -75,7 +75,7 @@ const TopNComponent: React.FC = ({ field, from, indexPattern, - indexToAdd, + indexNames, options, query = DEFAULT_QUERY, setAbsoluteRangeDatePicker, @@ -85,8 +85,10 @@ const TopNComponent: React.FC = ({ to, toggleTopN, }) => { - const [view, setView] = useState(defaultView); - const onViewSelected = useCallback((value: string) => setView(value as EventType), [setView]); + const [view, setView] = useState(defaultView); + const onViewSelected = useCallback((value: string) => setView(value as TimelineEventsType), [ + setView, + ]); useEffect(() => { setView(defaultView); @@ -123,7 +125,7 @@ const TopNComponent: React.FC = ({ from={from} headerChildren={headerChildren} indexPattern={indexPattern} - indexToAdd={indexToAdd} + indexNames={indexNames} onlyField={field} query={query} setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts index 5a4aec93dd9aa..e5c09d229808b 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts @@ -17,6 +17,7 @@ export enum CONSTANTS { networkPage = 'network.page', overviewPage = 'overview.page', savedQuery = 'savedQuery', + sourcerer = 'sourcerer', timeline = 'timeline', timelinePage = 'timeline.page', timerange = 'timerange', diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts index 6052913b4183b..a915b1c9d09a7 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts @@ -22,6 +22,8 @@ import { formatDate } from '../super_date_picker'; import { NavTab } from '../navigation/types'; import { CONSTANTS, UrlStateType } from './constants'; import { ReplaceStateInLocation, UpdateUrlStateString } from './types'; +import { sourcererSelectors } from '../../store/sourcerer'; +import { SourcererScopeName, SourcererScopePatterns } from '../../store/sourcerer/model'; export const decodeRisonUrlState = (value: string | undefined): T | null => { try { @@ -118,6 +120,7 @@ export const makeMapStateToProps = () => { const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); const getGlobalSavedQuerySelector = inputsSelectors.globalSavedQuerySelector(); const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const getSourcererScopes = sourcererSelectors.scopesSelector(); const mapStateToProps = (state: State) => { const inputState = getInputsSelector(state); const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global; @@ -147,10 +150,16 @@ export const makeMapStateToProps = () => { [CONSTANTS.savedQuery]: savedQuery.id, }; } + const sourcerer = getSourcererScopes(state); + const activeScopes: SourcererScopeName[] = Object.keys(sourcerer) as SourcererScopeName[]; + const selectedPatterns: SourcererScopePatterns = activeScopes + .filter((scope) => scope === SourcererScopeName.default) + .reduce((acc, scope) => ({ ...acc, [scope]: sourcerer[scope]?.selectedPatterns }), {}); return { urlState: { ...searchAttr, + [CONSTANTS.sourcerer]: selectedPatterns, [CONSTANTS.timerange]: { global: { [CONSTANTS.timerange]: globalTimerange, @@ -217,6 +226,17 @@ export const updateUrlStateString = ({ urlStateKey: urlKey, }); } + } else if (urlKey === CONSTANTS.sourcerer) { + const sourcererState = decodeRisonUrlState(newUrlStateString); + if (sourcererState != null && Object.keys(sourcererState).length > 0) { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: sourcererState, + urlStateKey: urlKey, + }); + } } else if (urlKey === CONSTANTS.filters) { const queryState = decodeRisonUrlState(newUrlStateString); if (isEmpty(queryState)) { diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx index 72df9d613abac..fc970c066e8a5 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx @@ -161,7 +161,7 @@ describe('UrlStateContainer', () => { ).toEqual({ hash: '', pathname: examplePath, - search: `?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, + search: `?query=(language:kuery,query:'host.name:%22siem-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, state: '', }); } diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx index 723f2d235864f..9e845ec538aa0 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx @@ -83,7 +83,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => hash: '', pathname: '/network', search: - "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "?query=(language:kuery,query:'host.name:%22siem-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", state: '', }); }); @@ -114,7 +114,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => hash: '', pathname: '/network', search: - "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + "?query=(language:kuery,query:'host.name:%22siem-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", state: '', }); }); @@ -147,7 +147,40 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => hash: '', pathname: '/network', search: - "?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))&timeline=(id:hello_timeline_id,isOpen:!t)", + "?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))&timeline=(id:hello_timeline_id,isOpen:!t)", + state: '', + }); + }); + + test('sourcerer redux state updates the url', () => { + mockProps = getMockPropsObj({ + page: CONSTANTS.networkPage, + examplePath: '/network', + namespaceLower: 'network', + pageName: SecurityPageName.network, + detailName: undefined, + }).noSearch.undefinedQuery; + + const wrapper = mount( + useUrlStateHooks(args)} /> + ); + const newUrlState = { + ...mockProps.urlState, + sourcerer: ['cool', 'patterns'], + }; + + wrapper.setProps({ + hookProps: { ...mockProps, urlState: newUrlState, isInitializing: false }, + }); + wrapper.update(); + + expect( + mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0] + ).toStrictEqual({ + hash: '', + pathname: '/network', + search: + "?sourcerer=!(cool,patterns)&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", state: '', }); }); @@ -176,7 +209,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => hash: '', pathname: examplePath, search: - "?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + "?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", state: '', }); } @@ -204,7 +237,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => expect( mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0].search ).toEqual( - "?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))" + "?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))" ); wrapper.setProps({ hookProps: updatedProps }); @@ -213,7 +246,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => expect( mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0].search ).toEqual( - "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))" + "?query=(language:kuery,query:'host.name:%22siem-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))" ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx index 6eccf52ec72da..1e77ae7766630 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx @@ -8,7 +8,7 @@ import { get, isEmpty } from 'lodash/fp'; import { Dispatch } from 'redux'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; -import { inputsActions } from '../../store/actions'; +import { inputsActions, sourcererActions } from '../../store/actions'; import { InputsModelId, TimeRangeKinds } from '../../store/inputs/constants'; import { UrlInputsModel, @@ -22,6 +22,8 @@ import { decodeRisonUrlState } from './helpers'; import { normalizeTimeRange } from './normalize_time_range'; import { DispatchSetInitialStateFromUrl, SetInitialStateFromUrl } from './types'; import { queryTimelineById } from '../../../timelines/components/open_timeline/helpers'; +import { SourcererScopeName, SourcererScopePatterns } from '../../store/sourcerer/model'; +import { SecurityPageName } from '../../../../common/constants'; export const dispatchSetInitialStateFromUrl = ( dispatch: Dispatch @@ -40,6 +42,22 @@ export const dispatchSetInitialStateFromUrl = ( if (urlKey === CONSTANTS.timerange) { updateTimerange(newUrlStateString, dispatch); } + if (urlKey === CONSTANTS.sourcerer) { + const sourcererState = decodeRisonUrlState(newUrlStateString); + if (sourcererState != null) { + const activeScopes: SourcererScopeName[] = Object.keys(sourcererState).filter( + (key) => !(key === SourcererScopeName.default && pageName === SecurityPageName.detections) + ) as SourcererScopeName[]; + activeScopes.forEach((scope) => + dispatch( + sourcererActions.setSelectedIndexPatterns({ + id: scope, + selectedPatterns: sourcererState[scope] ?? [], + }) + ) + ); + } + } if (urlKey === CONSTANTS.appQuery && indexPattern != null) { const appQuery = decodeRisonUrlState(newUrlStateString); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts b/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts index 8d471e843320c..6f04226fa3a19 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts @@ -117,6 +117,7 @@ export const defaultProps: UrlStateContainerPropTypes = { id: '', isOpen: false, }, + [CONSTANTS.sourcerer]: {}, }, setInitialStateFromUrl: dispatchSetInitialStateFromUrl(mockDispatch), updateTimeline: (jest.fn() as unknown) as DispatchUpdateTimeline, diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts index f383e18132385..301771a4db6b9 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts @@ -22,11 +22,13 @@ import { DispatchUpdateTimeline } from '../../../timelines/components/open_timel import { NavTab } from '../navigation/types'; import { CONSTANTS, UrlStateType } from './constants'; +import { SourcererScopePatterns } from '../../store/sourcerer/model'; export const ALL_URL_STATE_KEYS: KeyUrlState[] = [ CONSTANTS.appQuery, CONSTANTS.filters, CONSTANTS.savedQuery, + CONSTANTS.sourcerer, CONSTANTS.timerange, CONSTANTS.timeline, ]; @@ -36,6 +38,7 @@ export const URL_STATE_KEYS: Record = { CONSTANTS.appQuery, CONSTANTS.filters, CONSTANTS.savedQuery, + CONSTANTS.sourcerer, CONSTANTS.timerange, CONSTANTS.timeline, ], @@ -43,6 +46,7 @@ export const URL_STATE_KEYS: Record = { CONSTANTS.appQuery, CONSTANTS.filters, CONSTANTS.savedQuery, + CONSTANTS.sourcerer, CONSTANTS.timerange, CONSTANTS.timeline, ], @@ -51,6 +55,7 @@ export const URL_STATE_KEYS: Record = { CONSTANTS.appQuery, CONSTANTS.filters, CONSTANTS.savedQuery, + CONSTANTS.sourcerer, CONSTANTS.timerange, CONSTANTS.timeline, ], @@ -58,6 +63,7 @@ export const URL_STATE_KEYS: Record = { CONSTANTS.appQuery, CONSTANTS.filters, CONSTANTS.savedQuery, + CONSTANTS.sourcerer, CONSTANTS.timerange, CONSTANTS.timeline, ], @@ -65,6 +71,7 @@ export const URL_STATE_KEYS: Record = { CONSTANTS.appQuery, CONSTANTS.filters, CONSTANTS.savedQuery, + CONSTANTS.sourcerer, CONSTANTS.timerange, CONSTANTS.timeline, ], @@ -72,6 +79,7 @@ export const URL_STATE_KEYS: Record = { CONSTANTS.appQuery, CONSTANTS.filters, CONSTANTS.savedQuery, + CONSTANTS.sourcerer, CONSTANTS.timerange, CONSTANTS.timeline, ], @@ -93,6 +101,7 @@ export interface UrlState { [CONSTANTS.appQuery]?: Query; [CONSTANTS.filters]?: Filter[]; [CONSTANTS.savedQuery]?: string; + [CONSTANTS.sourcerer]: SourcererScopePatterns; [CONSTANTS.timerange]: UrlInputsModel; [CONSTANTS.timeline]: TimelineUrl; } diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx index f6ebbb990f223..489ccb23c9b2c 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx @@ -29,6 +29,7 @@ const AnomaliesQueryTabBodyComponent: React.FC = ({ AnomaliesTableComponent, flowTarget, ip, + indexNames, }) => { const { jobs } = useInstalledSecurityJobs(); const [anomalyScore] = useUiSetting$(DEFAULT_ANOMALY_SCORE); @@ -57,6 +58,7 @@ const AnomaliesQueryTabBodyComponent: React.FC = ({ endDate={endDate} filterQuery={mergedFilterQuery} id={ID} + indexNames={indexNames} setQuery={setQuery} startDate={startDate} {...histogramConfigs} diff --git a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts index d716df70246f7..3ce4b8b6d4494 100644 --- a/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts +++ b/x-pack/plugins/security_solution/public/common/containers/anomalies/anomalies_query_tab_body/types.ts @@ -24,6 +24,7 @@ export type AnomaliesQueryTabBodyProps = QueryTabBodyProps & { deleteQuery?: ({ id }: { id: string }) => void; endDate: GlobalTimeArgs['to']; flowTarget?: FlowTarget; + indexNames: string[]; narrowDateRange: NarrowDateRange; setQuery: GlobalTimeArgs['setQuery']; startDate: GlobalTimeArgs['from']; diff --git a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts index 3d79c83dc42cb..dc2d6605bc292 100644 --- a/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/events/last_event_time/index.ts @@ -8,7 +8,6 @@ import deepEqual from 'fast-deep-equal'; import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; import { inputsModel } from '../../../../common/store'; import { useKibana } from '../../../../common/lib/kibana'; import { @@ -23,10 +22,10 @@ import { isCompleteResponse, isErrorResponse, } from '../../../../../../../../src/plugins/data/common'; -import { useWithSource } from '../../source'; import * as i18n from './translations'; +import { DocValueFields } from '../../../../../common/search_strategy'; -// const ID = 'timelineEventsLastEventTimeQuery'; +const ID = 'timelineEventsLastEventTimeQuery'; export interface UseTimelineLastEventTimeArgs { lastSeen: string | null; @@ -35,26 +34,29 @@ export interface UseTimelineLastEventTimeArgs { } interface UseTimelineLastEventTimeProps { + docValueFields: DocValueFields[]; indexKey: LastEventIndexKey; + indexNames: string[]; details: LastTimeDetails; } export const useTimelineLastEventTime = ({ + docValueFields, indexKey, + indexNames, details, }: UseTimelineLastEventTimeProps): [boolean, UseTimelineLastEventTimeArgs] => { - const { data, notifications, uiSettings } = useKibana().services; - const { docValueFields } = useWithSource('default'); + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [TimelineLastEventTimeRequest, setTimelineLastEventTimeRequest] = useState< TimelineEventsLastEventTimeRequestOptions >({ - defaultIndex, - factoryQueryType: TimelineEventsQueries.lastEventTime, + defaultIndex: indexNames, docValueFields, + factoryQueryType: TimelineEventsQueries.lastEventTime, + id: ID, indexKey, details, }); @@ -133,7 +135,8 @@ export const useTimelineLastEventTime = ({ setTimelineLastEventTimeRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, + docValueFields, indexKey, details, }; @@ -142,7 +145,7 @@ export const useTimelineLastEventTime = ({ } return prevRequest; }); - }, [defaultIndex, details, indexKey]); + }, [indexNames, details, docValueFields, indexKey]); useEffect(() => { timelineLastEventTimeSearch(TimelineLastEventTimeRequest); diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts index 8e0c133f95b4d..ca8bcc637717b 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts @@ -5,14 +5,13 @@ */ import deepEqual from 'fast-deep-equal'; -import { isEmpty, noop } from 'lodash/fp'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { inputsModel } from '../../../common/store'; import { createFilter } from '../../../common/containers/helpers'; -import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; +import { useKibana } from '../../../common/lib/kibana'; import { MatrixHistogramQuery, MatrixHistogramRequestOptions, @@ -40,25 +39,18 @@ export const useMatrixHistogram = ({ errorMessage, filterQuery, histogramType, - indexToAdd, + indexNames, stackByField, startDate, }: MatrixHistogramQueryProps): [boolean, UseMatrixHistogramArgs] => { const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY); - const defaultIndex = useMemo(() => { - if (indexToAdd != null && !isEmpty(indexToAdd)) { - return [...configIndex, ...indexToAdd]; - } - return configIndex; - }, [configIndex, indexToAdd]); const [loading, setLoading] = useState(false); const [matrixHistogramRequest, setMatrixHistogramRequest] = useState< MatrixHistogramRequestOptions >({ - defaultIndex, + defaultIndex: indexNames, factoryQueryType: MatrixHistogramQuery, filterQuery: createFilter(filterQuery), histogramType, @@ -140,7 +132,7 @@ export const useMatrixHistogram = ({ setMatrixHistogramRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, filterQuery: createFilter(filterQuery), timerange: { interval: '12h', @@ -153,7 +145,7 @@ export const useMatrixHistogram = ({ } return prevRequest; }); - }, [defaultIndex, endDate, filterQuery, startDate]); + }, [indexNames, endDate, filterQuery, startDate]); useEffect(() => { hostsSearch(matrixHistogramRequest); diff --git a/x-pack/plugins/security_solution/public/common/containers/query_template.tsx b/x-pack/plugins/security_solution/public/common/containers/query_template.tsx index eaa43c255a944..80791d91481a8 100644 --- a/x-pack/plugins/security_solution/public/common/containers/query_template.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/query_template.tsx @@ -14,6 +14,7 @@ import { DocValueFields } from './source'; export { DocValueFields }; export interface QueryTemplateProps { + indexNames: string[]; docValueFields?: DocValueFields[]; id?: string; endDate?: string; diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.gql_query.ts b/x-pack/plugins/security_solution/public/common/containers/source/index.gql_query.ts deleted file mode 100644 index 630515c5cbed4..0000000000000 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.gql_query.ts +++ /dev/null @@ -1,31 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const sourceQuery = gql` - query SourceQuery($sourceId: ID = "default", $defaultIndex: [String!]!) { - source(id: $sourceId) { - id - status { - indicesExist(defaultIndex: $defaultIndex) - indexFields(defaultIndex: $defaultIndex) { - category - description - example - indexes - name - searchable - type - aggregatable - format - esTypes - subType - } - } - } - } -`; diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx deleted file mode 100644 index 8ba7f7da7b8e3..0000000000000 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.test.tsx +++ /dev/null @@ -1,109 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { act, renderHook } from '@testing-library/react-hooks'; - -import { useWithSource, indicesExistOrDataTemporarilyUnavailable } from '.'; -import { NO_ALERT_INDEX } from '../../../../common/constants'; -import { mockBrowserFields, mockIndexFields, mocksSource } from './mock'; - -jest.mock('../../lib/kibana'); -jest.mock('../../utils/apollo_context', () => ({ - useApolloClient: jest.fn().mockReturnValue({ - query: jest.fn().mockImplementation(() => Promise.resolve(mocksSource[0].result)), - }), -})); - -describe('Index Fields & Browser Fields', () => { - test('At initialization the value of indicesExists should be true', async () => { - const { result, waitForNextUpdate } = renderHook(() => useWithSource()); - const initialResult = result.current; - - await waitForNextUpdate(); - - return expect(initialResult).toEqual({ - browserFields: {}, - docValueFields: [], - errorMessage: null, - indexPattern: { - fields: [], - title: - 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*', - }, - indicesExist: true, - loading: true, - }); - }); - - test('returns memoized value', async () => { - const { result, waitForNextUpdate, rerender } = renderHook(() => useWithSource()); - await waitForNextUpdate(); - - const result1 = result.current; - act(() => rerender()); - const result2 = result.current; - - return expect(result1).toBe(result2); - }); - - test('Index Fields', async () => { - const { result, waitForNextUpdate } = renderHook(() => useWithSource()); - - await waitForNextUpdate(); - - return expect(result).toEqual({ - current: { - indicesExist: true, - browserFields: mockBrowserFields, - docValueFields: [ - { - field: '@timestamp', - format: 'date_time', - }, - { - field: 'event.end', - format: 'date_time', - }, - ], - indexPattern: { - fields: mockIndexFields, - title: - 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*', - }, - loading: false, - errorMessage: null, - }, - error: undefined, - }); - }); - - test('Make sure we are not querying for NO_ALERT_INDEX and it is not includes in the index pattern', async () => { - const { result, waitForNextUpdate } = renderHook(() => - useWithSource('default', [NO_ALERT_INDEX]) - ); - - await waitForNextUpdate(); - return expect(result.current.indexPattern.title).toEqual( - 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*' - ); - }); - - describe('indicesExistOrDataTemporarilyUnavailable', () => { - test('it returns true when undefined', () => { - let undefVar; - const result = indicesExistOrDataTemporarilyUnavailable(undefVar); - expect(result).toBeTruthy(); - }); - test('it returns true when true', () => { - const result = indicesExistOrDataTemporarilyUnavailable(true); - expect(result).toBeTruthy(); - }); - test('it returns false when false', () => { - const result = indicesExistOrDataTemporarilyUnavailable(false); - expect(result).toBeFalsy(); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index ffbecf9e3d433..4b1db8a2871bd 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -4,42 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isUndefined } from 'lodash'; import { set } from '@elastic/safer-lodash-set/fp'; -import { get, keyBy, pick, isEmpty } from 'lodash/fp'; -import { useEffect, useMemo, useState } from 'react'; +import { keyBy, pick, isEmpty, isEqual, isUndefined } from 'lodash/fp'; import memoizeOne from 'memoize-one'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; import { IIndexPattern } from 'src/plugins/data/public'; -import { DEFAULT_INDEX_KEY, NO_ALERT_INDEX } from '../../../../common/constants'; -import { useUiSetting$ } from '../../lib/kibana'; +import { useKibana } from '../../lib/kibana'; +import { + IndexField, + IndexFieldsStrategyResponse, + IndexFieldsStrategyRequest, + BrowserField, + BrowserFields, +} from '../../../../common/search_strategy/index_fields'; +import { AbortError } from '../../../../../../../src/plugins/data/common'; +import * as i18n from './translations'; +import { SourcererScopeName } from '../../store/sourcerer/model'; +import { sourcererActions, sourcererSelectors } from '../../store/sourcerer'; -import { IndexField, SourceQuery } from '../../../graphql/types'; +import { State } from '../../store'; +import { DocValueFields } from '../../../../common/search_strategy/common'; -import { sourceQuery } from './index.gql_query'; -import { useApolloClient } from '../../utils/apollo_context'; - -export { sourceQuery }; - -export interface BrowserField { - aggregatable: boolean; - category: string; - description: string | null; - example: string | number | null; - fields: Readonly>>; - format: string; - indexes: string[]; - name: string; - searchable: boolean; - type: string; -} - -export interface DocValueFields { - field: string; - format: string; -} - -export type BrowserFields = Readonly>>; +export { BrowserField, BrowserFields, DocValueFields }; export const getAllBrowserFields = (browserFields: BrowserFields): Array> => Object.values(browserFields).reduce>>( @@ -85,14 +73,12 @@ export const getDocValueFields = memoizeOne( (_title: string, fields: IndexField[]): DocValueFields[] => fields && fields.length > 0 ? fields.reduce((accumulator: DocValueFields[], field: IndexField) => { - if (field.type === 'date' && accumulator.length < 100) { - const format: string = - field.format != null && !isEmpty(field.format) ? field.format : 'date_time'; + if (field.readFromDocValues && accumulator.length < 100) { return [ ...accumulator, { field: field.name, - format, + format: field.format, }, ]; } @@ -107,115 +93,196 @@ export const indicesExistOrDataTemporarilyUnavailable = ( indicesExist: boolean | null | undefined ) => indicesExist || isUndefined(indicesExist); -const EMPTY_BROWSER_FIELDS = {}; -const EMPTY_DOCVALUE_FIELD: DocValueFields[] = []; +const DEFAULT_BROWSER_FIELDS = {}; +const DEFAULT_INDEX_PATTERNS = { fields: [], title: '' }; +const DEFAULT_DOC_VALUE_FIELDS: DocValueFields[] = []; -interface UseWithSourceState { +interface FetchIndexReturn { browserFields: BrowserFields; docValueFields: DocValueFields[]; - errorMessage: string | null; - indexPattern: IIndexPattern; - indicesExist: boolean | undefined | null; - loading: boolean; + indexes: string[]; + indexExists: boolean; + indexPatterns: IIndexPattern; } -export const useWithSource = ( - sourceId = 'default', - indexToAdd?: string[] | null, - onlyCheckIndexToAdd?: boolean, - // Fun fact: When using this hook multiple times within a component (e.g. add_exception_modal & edit_exception_modal), - // the apolloClient will perform queryDeduplication and prevent the first query from executing. A deep compare is not - // performed on `indices`, so another field must be passed to circumvent this. - // For details, see https://github.com/apollographql/react-apollo/issues/2202 - queryDeduplication = 'default' -) => { - const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY); - const defaultIndex = useMemo(() => { - const filterIndexAdd = (indexToAdd ?? []).filter((item) => item !== NO_ALERT_INDEX); - if (!isEmpty(filterIndexAdd)) { - return onlyCheckIndexToAdd ? filterIndexAdd : [...configIndex, ...filterIndexAdd]; - } - return configIndex; - }, [configIndex, indexToAdd, onlyCheckIndexToAdd]); - - const [state, setState] = useState({ - browserFields: EMPTY_BROWSER_FIELDS, - docValueFields: EMPTY_DOCVALUE_FIELD, - errorMessage: null, - indexPattern: getIndexFields(defaultIndex.join(), []), - indicesExist: indicesExistOrDataTemporarilyUnavailable(undefined), - loading: true, +export const useFetchIndex = ( + indexNames: string[], + onlyCheckIfIndicesExist: boolean = false +): [boolean, FetchIndexReturn] => { + const { data, notifications } = useKibana().services; + const abortCtrl = useRef(new AbortController()); + const previousIndexesName = useRef([]); + const [isLoading, setLoading] = useState(true); + + const [state, setState] = useState({ + browserFields: DEFAULT_BROWSER_FIELDS, + docValueFields: DEFAULT_DOC_VALUE_FIELDS, + indexes: indexNames, + indexExists: true, + indexPatterns: DEFAULT_INDEX_PATTERNS, }); - const apolloClient = useApolloClient(); + const indexFieldsSearch = useCallback( + (iNames) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + const searchSubscription$ = data.search + .search( + { indices: iNames, onlyCheckIfIndicesExist }, + { + abortSignal: abortCtrl.current.signal, + strategy: 'securitySolutionIndexFields', + } + ) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + const stringifyIndices = response.indicesExist.sort().join(); + previousIndexesName.current = response.indicesExist; + setLoading(false); + setState({ + browserFields: getBrowserFields(stringifyIndices, response.indexFields), + docValueFields: getDocValueFields(stringifyIndices, response.indexFields), + indexes: response.indicesExist, + indexExists: response.indicesExist.length > 0, + indexPatterns: getIndexFields(stringifyIndices, response.indexFields), + }); + } + searchSubscription$.unsubscribe(); + } else if (!didCancel && response.isPartial && !response.isRunning) { + setLoading(false); + notifications.toasts.addWarning(i18n.ERROR_BEAT_FIELDS); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!didCancel) { + setLoading(false); + } - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - - async function fetchSource() { - if (!apolloClient) return; - - setState((prevState) => ({ ...prevState, loading: true })); - - try { - const result = await apolloClient.query< - SourceQuery.Query, - SourceQuery.Variables & { queryDeduplication: string } - >({ - query: sourceQuery, - fetchPolicy: 'cache-first', - variables: { - sourceId, - defaultIndex, - queryDeduplication, - }, - context: { - fetchOptions: { - signal: abortCtrl.signal, + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + text: msg.message, + title: i18n.FAIL_BEAT_FIELDS, + }); + } }, - }, - }); - - if (isSubscribed) { - setState({ - loading: false, - indicesExist: indicesExistOrDataTemporarilyUnavailable( - get('data.source.status.indicesExist', result) - ), - browserFields: getBrowserFields( - defaultIndex.join(), - get('data.source.status.indexFields', result) - ), - docValueFields: getDocValueFields( - defaultIndex.join(), - get('data.source.status.indexFields', result) - ), - indexPattern: getIndexFields( - defaultIndex.join(), - get('data.source.status.indexFields', result) - ), - errorMessage: null, }); - } - } catch (error) { - if (isSubscribed) { - setState((prevState) => ({ - ...prevState, - loading: false, - errorMessage: error.message, - })); - } - } + }; + abortCtrl.current.abort(); + asyncSearch(); + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts, onlyCheckIfIndicesExist] + ); + + useEffect(() => { + if (!isEmpty(indexNames) && !isEqual(previousIndexesName.current, indexNames)) { + indexFieldsSearch(indexNames); } + }, [indexNames, indexFieldsSearch, previousIndexesName]); + + return [isLoading, state]; +}; + +export const useIndexFields = (sourcererScopeName: SourcererScopeName) => { + const { data, notifications } = useKibana().services; + const abortCtrl = useRef(new AbortController()); + const dispatch = useDispatch(); + const previousIndexesName = useRef([]); + + const indexNamesSelectedSelector = useMemo( + () => sourcererSelectors.getIndexNamesSelectedSelector(), + [] + ); + const indexNames = useSelector( + (state) => indexNamesSelectedSelector(state, sourcererScopeName), + shallowEqual + ); - fetchSource(); + const setLoading = useCallback( + (loading: boolean) => { + dispatch(sourcererActions.setSourcererScopeLoading({ id: sourcererScopeName, loading })); + }, + [dispatch, sourcererScopeName] + ); + + const indexFieldsSearch = useCallback( + (indicesName) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + const searchSubscription$ = data.search + .search( + { indices: indicesName, onlyCheckIfIndicesExist: false }, + { + abortSignal: abortCtrl.current.signal, + strategy: 'securitySolutionIndexFields', + } + ) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + const stringifyIndices = response.indicesExist.sort().join(); + previousIndexesName.current = response.indicesExist; + dispatch( + sourcererActions.setSource({ + id: sourcererScopeName, + payload: { + browserFields: getBrowserFields(stringifyIndices, response.indexFields), + docValueFields: getDocValueFields(stringifyIndices, response.indexFields), + errorMessage: null, + id: sourcererScopeName, + indexPattern: getIndexFields(stringifyIndices, response.indexFields), + indicesExist: response.indicesExist.length > 0, + loading: false, + }, + }) + ); + } + searchSubscription$.unsubscribe(); + } else if (!didCancel && response.isPartial && !response.isRunning) { + // TODO: Make response error status clearer + setLoading(false); + notifications.toasts.addWarning(i18n.ERROR_BEAT_FIELDS); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!didCancel) { + setLoading(false); + } - return () => { - isSubscribed = false; - return abortCtrl.abort(); - }; - }, [apolloClient, sourceId, defaultIndex, queryDeduplication]); + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + text: msg.message, + title: i18n.FAIL_BEAT_FIELDS, + }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, dispatch, notifications.toasts, setLoading, sourcererScopeName] + ); - return state; + useEffect(() => { + if (!isEmpty(indexNames) && !isEqual(previousIndexesName.current, indexNames)) { + indexFieldsSearch(indexNames); + } + }, [indexNames, indexFieldsSearch, previousIndexesName]); }; diff --git a/x-pack/plugins/security_solution/public/common/containers/source/mock.ts b/x-pack/plugins/security_solution/public/common/containers/source/mock.ts index bba6a15d73970..7fcd11f71f081 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/mock.ts +++ b/x-pack/plugins/security_solution/public/common/containers/source/mock.ts @@ -5,347 +5,296 @@ */ import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; +import { DocValueFields } from '../../../../common/search_strategy'; +import { BrowserFields } from '../../../../common/search_strategy/index_fields'; -import { BrowserFields, DocValueFields } from '.'; -import { sourceQuery } from './index.gql_query'; - -export const mocksSource = [ - { - request: { - query: sourceQuery, - variables: { - sourceId: 'default', - defaultIndex: DEFAULT_INDEX_PATTERN, - }, +export const mocksSource = { + indexFields: [ + { + category: 'base', + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: '@timestamp', + searchable: true, + type: 'date', + aggregatable: true, }, - result: { - data: { - source: { - id: 'default', - configuration: {}, - status: { - indicesExist: true, - winlogbeatIndices: [ - 'winlogbeat-7.0.0-2019.02.17', - 'winlogbeat-7.0.0-2019.02.18', - 'winlogbeat-7.0.0-2019.02.19', - 'winlogbeat-7.0.0-2019.02.20', - 'winlogbeat-7.0.0-2019.02.21', - 'winlogbeat-7.0.0-2019.02.21-000001', - 'winlogbeat-7.0.0-2019.02.22', - 'winlogbeat-8.0.0-2019.02.19-000001', - ], - auditbeatIndices: [ - 'auditbeat-7.0.0-2019.02.17', - 'auditbeat-7.0.0-2019.02.18', - 'auditbeat-7.0.0-2019.02.19', - 'auditbeat-7.0.0-2019.02.20', - 'auditbeat-7.0.0-2019.02.21', - 'auditbeat-7.0.0-2019.02.21-000001', - 'auditbeat-7.0.0-2019.02.22', - 'auditbeat-8.0.0-2019.02.19-000001', - ], - filebeatIndices: [ - 'filebeat-7.0.0-iot-2019.06', - 'filebeat-7.0.0-iot-2019.07', - 'filebeat-7.0.0-iot-2019.08', - 'filebeat-7.0.0-iot-2019.09', - 'filebeat-7.0.0-iot-2019.10', - 'filebeat-8.0.0-2019.02.19-000001', - ], - indexFields: [ - { - category: 'base', - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - example: '2016-05-23T08:05:34.853Z', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: '@timestamp', - searchable: true, - type: 'date', - aggregatable: true, - }, - { - category: 'agent', - description: - 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.ephemeral_id', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'agent', - description: null, - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.hostname', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'agent', - description: - 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', - example: '8a4f500d', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.id', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'agent', - description: - 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', - example: 'foo', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.name', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a0', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a1', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a2', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'client', - description: - 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.address', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'client', - description: 'Bytes sent from the client to the server.', - example: '184', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.bytes', - searchable: true, - type: 'number', - aggregatable: true, - }, - { - category: 'client', - description: 'Client domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.domain', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'client', - description: 'Country ISO code.', - example: 'CA', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.geo.country_iso_code', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'cloud', - description: - 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', - example: '666777888999', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.account.id', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'cloud', - description: 'Availability zone in which this host is running.', - example: 'us-east-1c', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.availability_zone', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'container', - description: 'Unique container id.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.id', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'container', - description: 'Name of the image the container was built on.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.image.name', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'container', - description: 'Container image tag.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.image.tag', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'destination', - description: - 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.address', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'destination', - description: 'Bytes sent from the destination to the source.', - example: '184', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.bytes', - searchable: true, - type: 'number', - aggregatable: true, - }, - { - category: 'destination', - description: 'Destination domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.domain', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - aggregatable: true, - category: 'destination', - description: - 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.ip', - searchable: true, - type: 'ip', - }, - { - aggregatable: true, - category: 'destination', - description: 'Port of the destination.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.port', - searchable: true, - type: 'long', - }, - { - aggregatable: true, - category: 'source', - description: - 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'source.ip', - searchable: true, - type: 'ip', - }, - { - aggregatable: true, - category: 'source', - description: 'Port of the source.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'source.port', - searchable: true, - type: 'long', - }, - { - aggregatable: true, - category: 'event', - description: - 'event.end contains the date when the event ended or when the activity was last observed.', - example: null, - format: '', - indexes: DEFAULT_INDEX_PATTERN, - name: 'event.end', - searchable: true, - type: 'date', - }, - ], - }, - }, - }, + { + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + aggregatable: true, }, - }, -]; + { + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'agent', + description: + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', + example: '8a4f500d', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'agent', + description: + 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', + example: 'foo', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.name', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a0', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a1', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a2', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'client', + description: + 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.address', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'client', + description: 'Bytes sent from the client to the server.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.bytes', + searchable: true, + type: 'number', + aggregatable: true, + }, + { + category: 'client', + description: 'Client domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.domain', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'client', + description: 'Country ISO code.', + example: 'CA', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.geo.country_iso_code', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'cloud', + description: + 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', + example: '666777888999', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.account.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'cloud', + description: 'Availability zone in which this host is running.', + example: 'us-east-1c', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.availability_zone', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'container', + description: 'Unique container id.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'container', + description: 'Name of the image the container was built on.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.name', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'container', + description: 'Container image tag.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.tag', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'destination', + description: + 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.address', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'destination', + description: 'Bytes sent from the destination to the source.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.bytes', + searchable: true, + type: 'number', + aggregatable: true, + }, + { + category: 'destination', + description: 'Destination domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.domain', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + aggregatable: true, + category: 'destination', + description: 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.ip', + searchable: true, + type: 'ip', + }, + { + aggregatable: true, + category: 'destination', + description: 'Port of the destination.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.port', + searchable: true, + type: 'long', + }, + { + aggregatable: true, + category: 'source', + description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.ip', + searchable: true, + type: 'ip', + }, + { + aggregatable: true, + category: 'source', + description: 'Port of the source.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.port', + searchable: true, + type: 'long', + }, + { + aggregatable: true, + category: 'event', + description: + 'event.end contains the date when the event ended or when the activity was last observed.', + example: null, + format: '', + indexes: DEFAULT_INDEX_PATTERN, + name: 'event.end', + searchable: true, + type: 'date', + }, + ], +}; export const mockIndexFields = [ { aggregatable: true, name: '@timestamp', searchable: true, type: 'date' }, diff --git a/x-pack/plugins/security_solution/public/common/containers/source/translations.ts b/x-pack/plugins/security_solution/public/common/containers/source/translations.ts new file mode 100644 index 0000000000000..f12a9a0b41a7b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/source/translations.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_BEAT_FIELDS = i18n.translate( + 'xpack.securitySolution.beatFields.errorSearchDescription', + { + defaultMessage: `An error has occurred on getting beat fields`, + } +); + +export const FAIL_BEAT_FIELDS = i18n.translate( + 'xpack.securitySolution.beatFields.failSearchDescription', + { + defaultMessage: `Failed to run search on beat fields`, + } +); diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/constants.ts b/x-pack/plugins/security_solution/public/common/containers/sourcerer/constants.ts index 106294ba54f5a..be3d074811032 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/constants.ts +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/constants.ts @@ -4,26 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const SOURCERER_FEATURE_FLAG_ON = false; - -export enum SecurityPageName { - default = 'default', - host = 'host', - detections = 'detections', - timeline = 'timeline', - network = 'network', -} - -export type SourceGroupsType = keyof typeof SecurityPageName; - -export const sourceGroups = { - [SecurityPageName.default]: [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'winlogbeat-*', - 'blobbeat-*', - ], -}; +export const SOURCERER_FEATURE_FLAG_ON = true; diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/format.test.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/format.test.tsx deleted file mode 100644 index b8017df09b738..0000000000000 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/format.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { indicesExistOrDataTemporarilyUnavailable } from './format'; - -describe('indicesExistOrDataTemporarilyUnavailable', () => { - it('it returns true when undefined', () => { - let undefVar; - const result = indicesExistOrDataTemporarilyUnavailable(undefVar); - expect(result).toBeTruthy(); - }); - it('it returns true when true', () => { - const result = indicesExistOrDataTemporarilyUnavailable(true); - expect(result).toBeTruthy(); - }); - it('it returns false when false', () => { - const result = indicesExistOrDataTemporarilyUnavailable(false); - expect(result).toBeFalsy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/format.ts b/x-pack/plugins/security_solution/public/common/containers/sourcerer/format.ts deleted file mode 100644 index 8c9a16ed705ef..0000000000000 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/format.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty, pick } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; -import { set } from '@elastic/safer-lodash-set/fp'; -import { isUndefined } from 'lodash'; -import { IndexField } from '../../../graphql/types'; -import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; - -export interface BrowserField { - aggregatable: boolean; - category: string; - description: string | null; - example: string | number | null; - fields: Readonly>>; - format: string; - indexes: string[]; - name: string; - searchable: boolean; - type: string; -} - -export interface DocValueFields { - field: string; - format: string; -} - -export type BrowserFields = Readonly>>; - -export const getAllBrowserFields = (browserFields: BrowserFields): Array> => - Object.values(browserFields).reduce>>( - (acc, namespace) => [ - ...acc, - ...Object.values(namespace.fields != null ? namespace.fields : {}), - ], - [] - ); - -export const getIndexFields = memoizeOne( - (title: string, fields: IndexField[]): IIndexPattern => - fields && fields.length > 0 - ? { - fields: fields.map((field) => - pick(['name', 'searchable', 'type', 'aggregatable', 'esTypes', 'subType'], field) - ), - title, - } - : { fields: [], title }, - (newArgs, lastArgs) => newArgs[0] === lastArgs[0] && newArgs[1].length === lastArgs[1].length -); - -export const getBrowserFields = memoizeOne( - (_title: string, fields: IndexField[]): BrowserFields => - fields && fields.length > 0 - ? fields.reduce( - (accumulator: BrowserFields, field: IndexField) => - set([field.category, 'fields', field.name], field, accumulator), - {} - ) - : {}, - // Update the value only if _title has changed - (newArgs, lastArgs) => newArgs[0] === lastArgs[0] -); - -export const getDocValueFields = memoizeOne( - (_title: string, fields: IndexField[]): DocValueFields[] => - fields && fields.length > 0 - ? fields.reduce((accumulator: DocValueFields[], field: IndexField) => { - if (field.type === 'date' && accumulator.length < 100) { - const format: string = - field.format != null && !isEmpty(field.format) ? field.format : 'date_time'; - return [ - ...accumulator, - { - field: field.name, - format, - }, - ]; - } - return accumulator; - }, []) - : [], - // Update the value only if _title has changed - (newArgs, lastArgs) => newArgs[0] === lastArgs[0] -); - -export const indicesExistOrDataTemporarilyUnavailable = ( - indicesExist: boolean | null | undefined -) => indicesExist || isUndefined(indicesExist); - -export const EMPTY_BROWSER_FIELDS = {}; -export const EMPTY_DOCVALUE_FIELD: DocValueFields[] = []; diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx index 38af84e0968f8..673db7af2b5e6 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx @@ -4,28 +4,73 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + +import React from 'react'; import { act, renderHook } from '@testing-library/react-hooks'; +import { Provider } from 'react-redux'; -import { getSourceDefaults, useSourceManager, UseSourceManager } from '.'; +import { useInitSourcerer } from '.'; +import { mockPatterns, mockSource } from './mocks'; +// import { SourcererScopeName } from '../../store/sourcerer/model'; +import { RouteSpyState } from '../../utils/route/types'; +import { SecurityPageName } from '../../../../common/constants'; +import { createStore, State } from '../../store'; import { - mockSourceSelections, - mockSourceGroup, - mockSourceGroups, - mockPatterns, - mockSource, -} from './mocks'; -import { SecurityPageName } from './constants'; -const mockSourceDefaults = mockSource(SecurityPageName.default); + apolloClientObservable, + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, +} from '../../mock'; +const mockSourceDefaults = mockSource; + +const mockRouteSpy: RouteSpyState = { + pageName: SecurityPageName.overview, + detailName: undefined, + tabName: undefined, + search: '', + pathName: '/', +}; +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); +jest.mock('../../utils/route/use_route_spy', () => ({ + useRouteSpy: () => [mockRouteSpy], +})); jest.mock('../../lib/kibana', () => ({ useKibana: jest.fn().mockReturnValue({ services: { + application: { + capabilities: { + siem: { + crud: true, + }, + }, + }, data: { indexPatterns: { getTitles: jest.fn().mockImplementation(() => Promise.resolve(mockPatterns)), }, + search: { + search: jest.fn().mockImplementation(() => ({ + subscribe: jest.fn().mockImplementation(() => ({ + error: jest.fn(), + next: jest.fn(), + })), + })), + }, }, + notifications: {}, }, }), + useUiSetting$: jest.fn().mockImplementation(() => [mockPatterns]), })); jest.mock('../../utils/apollo_context', () => ({ useApolloClient: jest.fn().mockReturnValue({ @@ -34,148 +79,193 @@ jest.mock('../../utils/apollo_context', () => ({ })); describe('Sourcerer Hooks', () => { - const testId = SecurityPageName.default; - const uninitializedId = SecurityPageName.host; + // const testId = SourcererScopeName.default; + // const uninitializedId = SourcererScopeName.detections; beforeEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); }); + const state: State = mockGlobalState; + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore( + state, + SUB_PLUGINS_REDUCER, + apolloClientObservable, + kibanaObservable, + storage + ); + + beforeEach(() => { + store = createStore( + state, + SUB_PLUGINS_REDUCER, + apolloClientObservable, + kibanaObservable, + storage + ); + }); describe('Initialization', () => { - it('initializes loading default index patterns', async () => { + it('initializes loading default and timeline index patterns', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useSourceManager() - ); - await waitForNextUpdate(); - expect(result.current).toEqual({ - activeSourceGroupId: 'default', - availableIndexPatterns: [], - availableSourceGroupIds: [], - isIndexPatternsLoading: true, - sourceGroups: {}, - getManageSourceGroupById: result.current.getManageSourceGroupById, - initializeSourceGroup: result.current.initializeSourceGroup, - setActiveSourceGroupId: result.current.setActiveSourceGroupId, - updateSourceGroupIndicies: result.current.updateSourceGroupIndicies, + const { waitForNextUpdate } = renderHook(() => useInitSourcerer(), { + wrapper: ({ children }) => {children}, }); - }); - }); - it('initializes loading default source group', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useSourceManager() - ); await waitForNextUpdate(); await waitForNextUpdate(); - expect(result.current).toEqual({ - activeSourceGroupId: 'default', - availableIndexPatterns: mockPatterns, - availableSourceGroupIds: [], - isIndexPatternsLoading: false, - sourceGroups: {}, - getManageSourceGroupById: result.current.getManageSourceGroupById, - initializeSourceGroup: result.current.initializeSourceGroup, - setActiveSourceGroupId: result.current.setActiveSourceGroupId, - updateSourceGroupIndicies: result.current.updateSourceGroupIndicies, + expect(mockDispatch).toBeCalledTimes(2); + expect(mockDispatch.mock.calls[0][0]).toEqual({ + type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING', + payload: { id: 'default', loading: true }, }); - }); - }); - it('initialize completes with formatted source group data', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useSourceManager() - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(result.current).toEqual({ - activeSourceGroupId: testId, - availableIndexPatterns: mockPatterns, - availableSourceGroupIds: [testId], - isIndexPatternsLoading: false, - sourceGroups: { - default: mockSourceGroup(testId), - }, - getManageSourceGroupById: result.current.getManageSourceGroupById, - initializeSourceGroup: result.current.initializeSourceGroup, - setActiveSourceGroupId: result.current.setActiveSourceGroupId, - updateSourceGroupIndicies: result.current.updateSourceGroupIndicies, + expect(mockDispatch.mock.calls[1][0]).toEqual({ + type: 'x-pack/security_solution/local/sourcerer/SET_SOURCERER_SCOPE_LOADING', + payload: { id: 'timeline', loading: true }, }); + // expect(mockDispatch.mock.calls[1][0]).toEqual({ + // type: 'x-pack/security_solution/local/sourcerer/SET_INDEX_PATTERNS_LIST', + // payload: { allIndexPatterns: mockPatterns, kibanaIndexPatterns: [] }, + // }); }); }); + // TO DO sourcerer @S + // it('initializes loading default source group', async () => { + // await act(async () => { + // const { result, waitForNextUpdate } = renderHook( + // () => useInitSourcerer(), + // { + // wrapper: ({ children }) => {children}, + // } + // ); + // await waitForNextUpdate(); + // await waitForNextUpdate(); + // expect(result.current).toEqual({ + // activeSourcererScopeId: 'default', + // kibanaIndexPatterns: mockPatterns, + // isIndexPatternsLoading: false, + // getSourcererScopeById: result.current.getSourcererScopeById, + // setActiveSourcererScopeId: result.current.setActiveSourcererScopeId, + // updateSourcererScopeIndices: result.current.updateSourcererScopeIndices, + // }); + // }); + // }); + // it('initialize completes with formatted source group data', async () => { + // await act(async () => { + // const { result, waitForNextUpdate } = renderHook( + // () => useInitSourcerer(), + // { + // wrapper: ({ children }) => {children}, + // } + // ); + // await waitForNextUpdate(); + // await waitForNextUpdate(); + // await waitForNextUpdate(); + // expect(result.current).toEqual({ + // activeSourcererScopeId: testId, + // kibanaIndexPatterns: mockPatterns, + // isIndexPatternsLoading: false, + // getSourcererScopeById: result.current.getSourcererScopeById, + // setActiveSourcererScopeId: result.current.setActiveSourcererScopeId, + // updateSourcererScopeIndices: result.current.updateSourcererScopeIndices, + // }); + // }); + // }); }); - describe('Methods', () => { - it('getManageSourceGroupById: initialized source group returns defaults', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useSourceManager() - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - await waitForNextUpdate(); - const initializedSourceGroup = result.current.getManageSourceGroupById(testId); - expect(initializedSourceGroup).toEqual(mockSourceGroup(testId)); - }); - }); - it('getManageSourceGroupById: uninitialized source group returns defaults', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useSourceManager() - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - await waitForNextUpdate(); - const uninitializedSourceGroup = result.current.getManageSourceGroupById(uninitializedId); - expect(uninitializedSourceGroup).toEqual(getSourceDefaults(uninitializedId, mockPatterns)); - }); - }); - it('initializeSourceGroup: initializes source group', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useSourceManager() - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - await waitForNextUpdate(); - result.current.initializeSourceGroup( - uninitializedId, - mockSourceGroups[uninitializedId], - true - ); - await waitForNextUpdate(); - const initializedSourceGroup = result.current.getManageSourceGroupById(uninitializedId); - expect(initializedSourceGroup.indexPatterns).toEqual(mockSourceSelections[uninitializedId]); - }); - }); - it('setActiveSourceGroupId: active source group id gets set only if it gets initialized first', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useSourceManager() - ); - await waitForNextUpdate(); - expect(result.current.activeSourceGroupId).toEqual(testId); - result.current.setActiveSourceGroupId(uninitializedId); - expect(result.current.activeSourceGroupId).toEqual(testId); - result.current.initializeSourceGroup(uninitializedId); - result.current.setActiveSourceGroupId(uninitializedId); - expect(result.current.activeSourceGroupId).toEqual(uninitializedId); - }); - }); - it('updateSourceGroupIndicies: updates source group indicies', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useSourceManager() - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - await waitForNextUpdate(); - let sourceGroup = result.current.getManageSourceGroupById(testId); - expect(sourceGroup.indexPatterns).toEqual(mockSourceSelections[testId]); - result.current.updateSourceGroupIndicies(testId, ['endgame-*', 'filebeat-*']); - await waitForNextUpdate(); - sourceGroup = result.current.getManageSourceGroupById(testId); - expect(sourceGroup.indexPatterns).toEqual(['endgame-*', 'filebeat-*']); - }); - }); - }); + // describe('Methods', () => { + // it('getSourcererScopeById: initialized source group returns defaults', async () => { + // await act(async () => { + // const { result, waitForNextUpdate } = renderHook( + // () => useInitSourcerer(), + // { + // wrapper: ({ children }) => {children}, + // } + // ); + // await waitForNextUpdate(); + // await waitForNextUpdate(); + // await waitForNextUpdate(); + // const initializedSourcererScope = result.current.getSourcererScopeById(testId); + // expect(initializedSourcererScope).toEqual(mockSourcererScope(testId)); + // }); + // }); + // it('getSourcererScopeById: uninitialized source group returns defaults', async () => { + // await act(async () => { + // const { result, waitForNextUpdate } = renderHook( + // () => useInitSourcerer(), + // { + // wrapper: ({ children }) => {children}, + // } + // ); + // await waitForNextUpdate(); + // await waitForNextUpdate(); + // await waitForNextUpdate(); + // const uninitializedSourcererScope = result.current.getSourcererScopeById(uninitializedId); + // expect(uninitializedSourcererScope).toEqual( + // getSourceDefaults(uninitializedId, mockPatterns) + // ); + // }); + // }); + // // it('initializeSourcererScope: initializes source group', async () => { + // // await act(async () => { + // // const { result, waitForNextUpdate } = renderHook( + // // () => useSourcerer(), + // // { + // // wrapper: ({ children }) => {children}, + // // } + // // ); + // // await waitForNextUpdate(); + // // await waitForNextUpdate(); + // // await waitForNextUpdate(); + // // result.current.initializeSourcererScope( + // // uninitializedId, + // // mockSourcererScopes[uninitializedId], + // // true + // // ); + // // await waitForNextUpdate(); + // // const initializedSourcererScope = result.current.getSourcererScopeById(uninitializedId); + // // expect(initializedSourcererScope.selectedPatterns).toEqual( + // // mockSourcererScopes[uninitializedId] + // // ); + // // }); + // // }); + // it('setActiveSourcererScopeId: active source group id gets set only if it gets initialized first', async () => { + // await act(async () => { + // const { result, waitForNextUpdate } = renderHook( + // () => useInitSourcerer(), + // { + // wrapper: ({ children }) => {children}, + // } + // ); + // await waitForNextUpdate(); + // expect(result.current.activeSourcererScopeId).toEqual(testId); + // result.current.setActiveSourcererScopeId(uninitializedId); + // expect(result.current.activeSourcererScopeId).toEqual(testId); + // // result.current.initializeSourcererScope(uninitializedId); + // result.current.setActiveSourcererScopeId(uninitializedId); + // expect(result.current.activeSourcererScopeId).toEqual(uninitializedId); + // }); + // }); + // it('updateSourcererScopeIndices: updates source group indices', async () => { + // await act(async () => { + // const { result, waitForNextUpdate } = renderHook( + // () => useInitSourcerer(), + // { + // wrapper: ({ children }) => {children}, + // } + // ); + // await waitForNextUpdate(); + // await waitForNextUpdate(); + // await waitForNextUpdate(); + // let sourceGroup = result.current.getSourcererScopeById(testId); + // expect(sourceGroup.selectedPatterns).toEqual(mockSourcererScopes[testId]); + // expect(sourceGroup.scopePatterns).toEqual(mockSourcererScopes[testId]); + // result.current.updateSourcererScopeIndices({ + // id: testId, + // selectedPatterns: ['endgame-*', 'filebeat-*'], + // }); + // await waitForNextUpdate(); + // sourceGroup = result.current.getSourcererScopeById(testId); + // expect(sourceGroup.scopePatterns).toEqual(mockSourcererScopes[testId]); + // expect(sourceGroup.selectedPatterns).toEqual(['endgame-*', 'filebeat-*']); + // }); + // }); + // }); }); diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx index 91907b45aa449..afacd68d71592 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx @@ -4,412 +4,72 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, noop, isEmpty } from 'lodash/fp'; -import React, { createContext, useCallback, useContext, useEffect, useReducer } from 'react'; -import { IIndexPattern } from 'src/plugins/data/public'; - -import { NO_ALERT_INDEX } from '../../../../common/constants'; -import { useKibana } from '../../lib/kibana'; - -import { SourceQuery } from '../../../graphql/types'; - -import { sourceQuery } from '../source/index.gql_query'; -import { useApolloClient } from '../../utils/apollo_context'; -import { - sourceGroups, - SecurityPageName, - SourceGroupsType, - SOURCERER_FEATURE_FLAG_ON, -} from './constants'; -import { - BrowserFields, - DocValueFields, - EMPTY_BROWSER_FIELDS, - EMPTY_DOCVALUE_FIELD, - getBrowserFields, - getDocValueFields, - getIndexFields, - indicesExistOrDataTemporarilyUnavailable, -} from './format'; - -// TYPES -interface ManageSource { - browserFields: BrowserFields; - defaultPatterns: string[]; - docValueFields: DocValueFields[]; - errorMessage: string | null; - id: SourceGroupsType; - indexPattern: IIndexPattern; - indexPatterns: string[]; - indicesExist: boolean | undefined | null; - loading: boolean; -} - -interface ManageSourceInit extends Partial { - id: SourceGroupsType; -} - -type ManageSourceGroupById = { - [id in SourceGroupsType]?: ManageSource; -}; - -type ActionManageSource = - | { - type: 'SET_SOURCE'; - id: SourceGroupsType; - defaultIndex: string[]; - payload: ManageSourceInit; - } - | { - type: 'SET_IS_SOURCE_LOADING'; - id: SourceGroupsType; - payload: boolean; - } - | { - type: 'SET_ACTIVE_SOURCE_GROUP_ID'; - payload: SourceGroupsType; - } - | { - type: 'SET_AVAILABLE_INDEX_PATTERNS'; - payload: string[]; - } - | { - type: 'SET_IS_INDEX_PATTERNS_LOADING'; - payload: boolean; - }; - -interface ManageSourcerer { - activeSourceGroupId: SourceGroupsType; - availableIndexPatterns: string[]; - availableSourceGroupIds: SourceGroupsType[]; - isIndexPatternsLoading: boolean; - sourceGroups: ManageSourceGroupById; -} - -export interface UseSourceManager extends ManageSourcerer { - getManageSourceGroupById: (id: SourceGroupsType) => ManageSource; - initializeSourceGroup: ( - id: SourceGroupsType, - indexToAdd?: string[] | null, - onlyCheckIndexToAdd?: boolean - ) => void; - setActiveSourceGroupId: (id: SourceGroupsType) => void; - updateSourceGroupIndicies: (id: SourceGroupsType, updatedIndicies: string[]) => void; -} - -// DEFAULTS/INIT -export const getSourceDefaults = (id: SourceGroupsType, defaultIndex: string[]) => ({ - browserFields: EMPTY_BROWSER_FIELDS, - defaultPatterns: defaultIndex, - docValueFields: EMPTY_DOCVALUE_FIELD, - errorMessage: null, - id, - indexPattern: getIndexFields(defaultIndex.join(), []), - indexPatterns: defaultIndex, - indicesExist: indicesExistOrDataTemporarilyUnavailable(undefined), - loading: true, -}); - -const initManageSource: ManageSourcerer = { - activeSourceGroupId: SecurityPageName.default, - availableIndexPatterns: [], - availableSourceGroupIds: [], - isIndexPatternsLoading: true, - sourceGroups: {}, -}; -const init: UseSourceManager = { - ...initManageSource, - getManageSourceGroupById: (id: SourceGroupsType) => getSourceDefaults(id, []), - initializeSourceGroup: () => noop, - setActiveSourceGroupId: () => noop, - updateSourceGroupIndicies: () => noop, -}; - -const reducerManageSource = (state: ManageSourcerer, action: ActionManageSource) => { - switch (action.type) { - case 'SET_SOURCE': - return { - ...state, - sourceGroups: { - ...state.sourceGroups, - [action.id]: { - ...getSourceDefaults(action.id, action.defaultIndex), - ...state.sourceGroups[action.id], - ...action.payload, - }, - }, - availableSourceGroupIds: state.availableSourceGroupIds.includes(action.id) - ? state.availableSourceGroupIds - : [...state.availableSourceGroupIds, action.id], - }; - case 'SET_IS_SOURCE_LOADING': - return { - ...state, - sourceGroups: { - ...state.sourceGroups, - [action.id]: { - ...state.sourceGroups[action.id], - id: action.id, - loading: action.payload, - }, - }, - }; - case 'SET_ACTIVE_SOURCE_GROUP_ID': - return { - ...state, - activeSourceGroupId: action.payload, - }; - case 'SET_AVAILABLE_INDEX_PATTERNS': - return { - ...state, - availableIndexPatterns: action.payload, - }; - case 'SET_IS_INDEX_PATTERNS_LOADING': - return { - ...state, - isIndexPatternsLoading: action.payload, - }; - default: - return state; - } -}; - -// HOOKS -export const useSourceManager = (): UseSourceManager => { - const { - services: { - data: { indexPatterns }, - }, - } = useKibana(); - const apolloClient = useApolloClient(); - const [state, dispatch] = useReducer(reducerManageSource, initManageSource); - - // Kibana Index Patterns - const setIsIndexPatternsLoading = useCallback((loading: boolean) => { - dispatch({ - type: 'SET_IS_INDEX_PATTERNS_LOADING', - payload: loading, - }); - }, []); - const getDefaultIndex = useCallback( - (indexToAdd?: string[] | null, onlyCheckIndexToAdd?: boolean) => { - const filterIndexAdd = (indexToAdd ?? []).filter((item) => item !== NO_ALERT_INDEX); - if (!isEmpty(filterIndexAdd)) { - return onlyCheckIndexToAdd - ? filterIndexAdd.sort() - : [ - ...state.availableIndexPatterns, - ...filterIndexAdd.filter((index) => !state.availableIndexPatterns.includes(index)), - ].sort(); - } - return state.availableIndexPatterns.sort(); - }, - [state.availableIndexPatterns] - ); - const setAvailableIndexPatterns = useCallback((availableIndexPatterns: string[]) => { - dispatch({ - type: 'SET_AVAILABLE_INDEX_PATTERNS', - payload: availableIndexPatterns, - }); - }, []); - const fetchKibanaIndexPatterns = useCallback(() => { - setIsIndexPatternsLoading(true); - const abortCtrl = new AbortController(); - - async function fetchTitles() { - try { - const result = await indexPatterns.getTitles(); - setAvailableIndexPatterns(result); - setIsIndexPatternsLoading(false); - } catch (error) { - setIsIndexPatternsLoading(false); - } - } - - fetchTitles(); - - return () => { - return abortCtrl.abort(); - }; - }, [indexPatterns, setAvailableIndexPatterns, setIsIndexPatternsLoading]); - - // Security Solution Source Groups - const setActiveSourceGroupId = useCallback( - (sourceGroupId: SourceGroupsType) => { - if (state.availableSourceGroupIds.includes(sourceGroupId)) { - dispatch({ - type: 'SET_ACTIVE_SOURCE_GROUP_ID', - payload: sourceGroupId, - }); - } - }, - [state.availableSourceGroupIds] - ); - const setIsSourceLoading = useCallback( - ({ id, loading }: { id: SourceGroupsType; loading: boolean }) => { - dispatch({ - type: 'SET_IS_SOURCE_LOADING', - id, - payload: loading, - }); - }, +import deepEqual from 'fast-deep-equal'; +import isEqual from 'lodash/isEqual'; +import { useEffect, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { sourcererActions, sourcererSelectors } from '../../store/sourcerer'; +import { ManageScope, SourcererScopeName } from '../../store/sourcerer/model'; +import { useIndexFields } from '../source'; +import { State } from '../../store'; +import { useUserInfo } from '../../../detections/components/user_info'; + +export const useInitSourcerer = ( + scopeId: SourcererScopeName.default | SourcererScopeName.detections = SourcererScopeName.default +) => { + const dispatch = useDispatch(); + + const { loading: loadingSignalIndex, isSignalIndexExists, signalIndexName } = useUserInfo(); + const getConfigIndexPatternsSelector = useMemo( + () => sourcererSelectors.configIndexPatternsSelector(), [] ); - const enrichSource = useCallback( - (id: SourceGroupsType, indexToAdd?: string[] | null, onlyCheckIndexToAdd?: boolean) => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - const defaultIndex = getDefaultIndex(indexToAdd, onlyCheckIndexToAdd); - const selectedPatterns = defaultIndex.filter((pattern) => - state.availableIndexPatterns.includes(pattern) - ); - if (state.sourceGroups[id] == null) { - dispatch({ - type: 'SET_SOURCE', - id, - defaultIndex: selectedPatterns, - payload: { defaultPatterns: defaultIndex, id }, - }); - } - - async function fetchSource() { - if (!apolloClient) return; - setIsSourceLoading({ id, loading: true }); - try { - const result = await apolloClient.query({ - query: sourceQuery, - fetchPolicy: 'network-only', - variables: { - sourceId: 'default', // always - defaultIndex: selectedPatterns, - }, - context: { - fetchOptions: { - signal: abortCtrl.signal, - }, - }, - }); - if (isSubscribed) { - dispatch({ - type: 'SET_SOURCE', - id, - defaultIndex: selectedPatterns, - payload: { - browserFields: getBrowserFields( - selectedPatterns.join(), - get('data.source.status.indexFields', result) - ), - docValueFields: getDocValueFields( - selectedPatterns.join(), - get('data.source.status.indexFields', result) - ), - errorMessage: null, - id, - indexPattern: getIndexFields( - selectedPatterns.join(), - get('data.source.status.indexFields', result) - ), - indexPatterns: selectedPatterns, - indicesExist: indicesExistOrDataTemporarilyUnavailable( - get('data.source.status.indicesExist', result) - ), - loading: false, - }, - }); - } - } catch (error) { - if (isSubscribed) { - dispatch({ - type: 'SET_SOURCE', - id, - defaultIndex: selectedPatterns, - payload: { - errorMessage: error.message, - id, - loading: false, - }, - }); - } - } - } - - fetchSource(); - - return () => { - isSubscribed = false; - return abortCtrl.abort(); - }; - }, - [ - apolloClient, - getDefaultIndex, - setIsSourceLoading, - state.availableIndexPatterns, - state.sourceGroups, - ] - ); + const ConfigIndexPatterns = useSelector(getConfigIndexPatternsSelector, isEqual); - const initializeSourceGroup = useCallback( - (id: SourceGroupsType, indexToAdd?: string[] | null, onlyCheckIndexToAdd?: boolean) => - enrichSource(id, indexToAdd, onlyCheckIndexToAdd), - [enrichSource] - ); - - const updateSourceGroupIndicies = useCallback( - (id: SourceGroupsType, updatedIndicies: string[]) => enrichSource(id, updatedIndicies, true), - [enrichSource] - ); - const getManageSourceGroupById = useCallback( - (id: SourceGroupsType) => { - const sourceById = state.sourceGroups[id]; - if (sourceById != null) { - return sourceById; - } - return getSourceDefaults(id, getDefaultIndex()); - }, - [getDefaultIndex, state.sourceGroups] - ); + useIndexFields(scopeId); + useIndexFields(SourcererScopeName.timeline); - // load initial default index useEffect(() => { - fetchKibanaIndexPatterns(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + if (!loadingSignalIndex && signalIndexName != null) { + dispatch(sourcererActions.setSignalIndexName({ signalIndexName })); + } + }, [dispatch, loadingSignalIndex, signalIndexName]); + // Related to timeline useEffect(() => { - if (!state.isIndexPatternsLoading) { - Object.entries(sourceGroups).forEach(([key, value]) => - initializeSourceGroup(key as SourceGroupsType, value, true) + if (!loadingSignalIndex && signalIndexName != null) { + dispatch( + sourcererActions.setSelectedIndexPatterns({ + id: SourcererScopeName.timeline, + selectedPatterns: [...ConfigIndexPatterns, signalIndexName], + }) ); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [state.isIndexPatternsLoading]); + }, [ConfigIndexPatterns, dispatch, loadingSignalIndex, signalIndexName]); - return { - ...state, - getManageSourceGroupById, - initializeSourceGroup, - setActiveSourceGroupId, - updateSourceGroupIndicies, - }; + // Related to the detection page + useEffect(() => { + if ( + scopeId === SourcererScopeName.detections && + isSignalIndexExists && + signalIndexName != null + ) { + dispatch( + sourcererActions.setSelectedIndexPatterns({ + id: scopeId, + selectedPatterns: [signalIndexName], + }) + ); + } + }, [dispatch, isSignalIndexExists, scopeId, signalIndexName]); }; -const ManageSourceContext = createContext(init); - -export const useManageSource = () => useContext(ManageSourceContext); - -interface ManageSourceProps { - children: React.ReactNode; -} - -export const MaybeManageSource = ({ children }: ManageSourceProps) => { - const indexPatternManager = useSourceManager(); - return ( - - {children} - +export const useSourcererScope = (scope: SourcererScopeName = SourcererScopeName.default) => { + const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []); + const SourcererScope = useSelector( + (state) => sourcererScopeSelector(state, scope), + deepEqual ); + return SourcererScope; }; -export const ManageSource = SOURCERER_FEATURE_FLAG_ON - ? MaybeManageSource - : ({ children }: ManageSourceProps) => <>{children}; diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/mocks.ts b/x-pack/plugins/security_solution/public/common/containers/sourcerer/mocks.ts index cde14e54694f0..c34a6917f300e 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/mocks.ts +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/mocks.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SecurityPageName } from './constants'; -import { getSourceDefaults } from './index'; +import { initSourcererScope } from '../../store/sourcerer/model'; export const mockPatterns = [ 'auditbeat-*', @@ -14,32 +13,10 @@ export const mockPatterns = [ 'logs-*', 'packetbeat-*', 'winlogbeat-*', + 'journalbeat-*', ]; -export const mockSourceGroups = { - [SecurityPageName.default]: [ - 'apm-*-transaction*', - 'auditbeat-*', - 'blobbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'winlogbeat-*', - ], - [SecurityPageName.host]: [ - 'apm-*-transaction*', - 'endgame-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', - ], -}; - -export const mockSourceSelections = { - [SecurityPageName.default]: ['auditbeat-*', 'endgame-*', 'filebeat-*', 'logs-*', 'winlogbeat-*'], - [SecurityPageName.host]: ['endgame-*', 'logs-*', 'packetbeat-*', 'winlogbeat-*'], -}; -export const mockSource = (testId: SecurityPageName.default | SecurityPageName.host) => ({ +export const mockSource = { data: { source: { id: 'default', @@ -50,7 +27,7 @@ export const mockSource = (testId: SecurityPageName.default | SecurityPageName.h category: '_id', description: 'Each document has an _id that uniquely identifies it', example: 'Y-6TfmcB0WOhS6qyMv3s', - indexes: mockSourceSelections[testId], + indexes: mockPatterns, name: '_id', searchable: true, type: 'string', @@ -67,48 +44,45 @@ export const mockSource = (testId: SecurityPageName.default | SecurityPageName.h loading: false, networkStatus: 7, stale: false, -}); +}; -export const mockSourceGroup = (testId: SecurityPageName.default | SecurityPageName.host) => { - const indexes = mockSourceSelections[testId]; - return { - ...getSourceDefaults(testId, mockPatterns), - defaultPatterns: mockSourceGroups[testId], - browserFields: { - _id: { - fields: { - _id: { - __typename: 'IndexField', - aggregatable: false, - category: '_id', - description: 'Each document has an _id that uniquely identifies it', - esTypes: null, - example: 'Y-6TfmcB0WOhS6qyMv3s', - format: null, - indexes, - name: '_id', - searchable: true, - subType: null, - type: 'string', - }, - }, - }, - }, - indexPattern: { - fields: [ - { +export const mockSourcererScope = { + ...initSourcererScope, + scopePatterns: mockPatterns, + browserFields: { + _id: { + fields: { + _id: { + __typename: 'IndexField', aggregatable: false, + category: '_id', + description: 'Each document has an _id that uniquely identifies it', esTypes: null, + example: 'Y-6TfmcB0WOhS6qyMv3s', + format: null, + indexes: mockPatterns, name: '_id', searchable: true, subType: null, type: 'string', }, - ], - title: indexes.join(), + }, }, - indexPatterns: indexes, - indicesExist: true, - loading: false, - }; + }, + indexPattern: { + fields: [ + { + aggregatable: false, + esTypes: null, + name: '_id', + searchable: true, + subType: null, + type: 'string', + }, + ], + title: mockPatterns.join(), + }, + selectedPatterns: mockPatterns, + indicesExist: true, + loading: false, }; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts index 573ef92f7e069..3051459d5de0c 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts @@ -12,9 +12,25 @@ import { createStartServicesMock, createWithKibanaMock, } from '../kibana_react.mock'; - +const mockStartServicesMock = createStartServicesMock(); export const KibanaServices = { get: jest.fn(), getKibanaVersion: jest.fn(() => '8.0.0') }; -export const useKibana = jest.fn().mockReturnValue({ services: createStartServicesMock() }); +export const useKibana = jest.fn().mockReturnValue({ + services: { + ...mockStartServicesMock, + data: { + ...mockStartServicesMock.data, + search: { + ...mockStartServicesMock.data.search, + search: jest.fn().mockImplementation(() => ({ + subscribe: jest.fn().mockImplementation(() => ({ + error: jest.fn(), + next: jest.fn(), + })), + })), + }, + }, + }, +}); export const useUiSetting = jest.fn(createUseUiSettingMock()); export const useUiSetting$ = jest.fn(createUseUiSetting$Mock()); export const useHttp = jest.fn().mockReturnValue(createStartServicesMock().http); diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index a74c9a6d2009d..0944b6aa27f67 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -22,11 +22,15 @@ import { DEFAULT_TO, DEFAULT_INTERVAL_TYPE, DEFAULT_INTERVAL_VALUE, + DEFAULT_INDEX_PATTERN, } from '../../../common/constants'; import { networkModel } from '../../network/store'; import { TimelineType, TimelineStatus } from '../../../common/types/timeline'; import { mockManagementState } from '../../management/store/reducer'; import { ManagementState } from '../../management/types'; +import { initialSourcererState, SourcererScopeName } from '../store/sourcerer/model'; +import { mockBrowserFields, mockDocValueFields } from '../containers/source/mock'; +import { mockIndexPattern } from './index_pattern'; export const mockGlobalState: State = { app: { @@ -203,6 +207,7 @@ export const mockGlobalState: State = { id: 'test', savedObjectId: null, columns: defaultHeaders, + indexNames: DEFAULT_INDEX_PATTERN, itemsPerPage: 5, dataProviders: [], description: '', @@ -241,6 +246,28 @@ export const mockGlobalState: State = { }, insertTimeline: null, }, + sourcerer: { + ...initialSourcererState, + sourcererScopes: { + ...initialSourcererState.sourcererScopes, + [SourcererScopeName.default]: { + ...initialSourcererState.sourcererScopes[SourcererScopeName.default], + selectedPatterns: DEFAULT_INDEX_PATTERN, + browserFields: mockBrowserFields, + indexPattern: mockIndexPattern, + docValueFields: mockDocValueFields, + loading: false, + }, + [SourcererScopeName.timeline]: { + ...initialSourcererState.sourcererScopes[SourcererScopeName.timeline], + selectedPatterns: DEFAULT_INDEX_PATTERN, + browserFields: mockBrowserFields, + indexPattern: mockIndexPattern, + docValueFields: mockDocValueFields, + loading: false, + }, + }, + }, /** * These state's are wrapped in `Immutable`, but for compatibility with the overall app architecture, * they are cast to mutable versions here. diff --git a/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts b/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts index 826057560f942..e4abc17e9034c 100644 --- a/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts +++ b/x-pack/plugins/security_solution/public/common/mock/index_pattern.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export const mockIndexPattern = { +import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; + +export const mockIndexPattern: IIndexPattern = { fields: [ { name: '@timestamp', @@ -93,3 +95,5 @@ export const mockIndexPattern = { ], title: 'filebeat-*,auditbeat-*,packetbeat-*', }; + +export const mockIndexNames = ['filebeat-*', 'auditbeat-*', 'packetbeat-*']; diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index 26013915315af..6403a50ad4a1d 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -2124,6 +2124,7 @@ export const mockTimelineModel: TimelineModel = { highlightedDropAndProviderId: '', historyIds: [], id: 'ef579e40-jibber-jabber', + indexNames: [], isFavorite: false, isLive: false, isLoading: false, @@ -2228,6 +2229,7 @@ export const defaultTimelineProps: CreateTimelineProps = { highlightedDropAndProviderId: '', historyIds: [], id: TimelineId.active, + indexNames: [], isFavorite: false, isLive: false, isLoading: false, diff --git a/x-pack/plugins/security_solution/public/common/store/actions.ts b/x-pack/plugins/security_solution/public/common/store/actions.ts index 6b446ab6692d9..f4134b5c47c2c 100644 --- a/x-pack/plugins/security_solution/public/common/store/actions.ts +++ b/x-pack/plugins/security_solution/public/common/store/actions.ts @@ -12,6 +12,7 @@ import { TrustedAppsPageAction } from '../../management/pages/trusted_apps/store export { appActions } from './app'; export { dragAndDropActions } from './drag_and_drop'; export { inputsActions } from './inputs'; +export { sourcererActions } from './sourcerer'; import { RoutingAction } from './routing'; export type AppAction = diff --git a/x-pack/plugins/security_solution/public/common/store/model.ts b/x-pack/plugins/security_solution/public/common/store/model.ts index 0032a95cce321..04603d0607583 100644 --- a/x-pack/plugins/security_solution/public/common/store/model.ts +++ b/x-pack/plugins/security_solution/public/common/store/model.ts @@ -7,4 +7,5 @@ export { appModel } from './app'; export { dragAndDropModel } from './drag_and_drop'; export { inputsModel } from './inputs'; +export { sourcererModel } from './sourcerer'; export * from './types'; diff --git a/x-pack/plugins/security_solution/public/common/store/reducer.ts b/x-pack/plugins/security_solution/public/common/store/reducer.ts index a0977cea71da7..60cb6a4e960bd 100644 --- a/x-pack/plugins/security_solution/public/common/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/reducer.ts @@ -9,6 +9,7 @@ import { combineReducers, PreloadedState, AnyAction, Reducer } from 'redux'; import { appReducer, initialAppState } from './app'; import { dragAndDropReducer, initialDragAndDropState } from './drag_and_drop'; import { createInitialInputsState, inputsReducer } from './inputs'; +import { sourcererReducer, sourcererModel } from './sourcerer'; import { HostsPluginReducer } from '../../hosts/store'; import { NetworkPluginReducer } from '../../network/store'; @@ -18,6 +19,7 @@ import { SecuritySubPlugins } from '../../app/types'; import { ManagementPluginReducer } from '../../management'; import { State } from './types'; import { AppAction } from './actions'; +import { KibanaIndexPatterns } from './sourcerer/model'; export type SubPluginsInitReducer = HostsPluginReducer & NetworkPluginReducer & @@ -28,13 +30,22 @@ export type SubPluginsInitReducer = HostsPluginReducer & * Factory for the 'initialState' that is used to preload state into the Security App's redux store. */ export const createInitialState = ( - pluginsInitState: SecuritySubPlugins['store']['initialState'] + pluginsInitState: SecuritySubPlugins['store']['initialState'], + { + kibanaIndexPatterns, + configIndexPatterns, + }: { kibanaIndexPatterns: KibanaIndexPatterns; configIndexPatterns: string[] } ): PreloadedState => { const preloadedState: PreloadedState = { app: initialAppState, dragAndDrop: initialDragAndDropState, ...pluginsInitState, inputs: createInitialInputsState(), + sourcerer: { + ...sourcererModel.initialSourcererState, + kibanaIndexPatterns, + configIndexPatterns, + }, }; return preloadedState; }; @@ -49,5 +60,6 @@ export const createReducer: ( app: appReducer, dragAndDrop: dragAndDropReducer, inputs: inputsReducer, + sourcerer: sourcererReducer, ...pluginsReducer, }); diff --git a/x-pack/plugins/security_solution/public/common/store/selectors.ts b/x-pack/plugins/security_solution/public/common/store/selectors.ts index b938bae39b634..3cefd92bf9e60 100644 --- a/x-pack/plugins/security_solution/public/common/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/selectors.ts @@ -7,3 +7,4 @@ export { appSelectors } from './app'; export { dragAndDropSelectors } from './drag_and_drop'; export { inputsSelectors } from './inputs'; +export { sourcererSelectors } from './sourcerer'; diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.ts new file mode 100644 index 0000000000000..0b40586798f09 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/actions.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import actionCreatorFactory from 'typescript-fsa'; +import { TimelineEventsType } from '../../../../common/types/timeline'; + +import { KibanaIndexPatterns, ManageScopeInit, SourcererScopeName } from './model'; + +const actionCreator = actionCreatorFactory('x-pack/security_solution/local/sourcerer'); + +export const setSource = actionCreator<{ + id: SourcererScopeName; + payload: ManageScopeInit; +}>('SET_SOURCE'); + +export const setIndexPatternsList = actionCreator<{ + kibanaIndexPatterns: KibanaIndexPatterns; + configIndexPatterns: string[]; +}>('SET_INDEX_PATTERNS_LIST'); + +export const setSignalIndexName = actionCreator<{ signalIndexName: string }>( + 'SET_SIGNAL_INDEX_NAME' +); + +export const setSourcererScopeLoading = actionCreator<{ id: SourcererScopeName; loading: boolean }>( + 'SET_SOURCERER_SCOPE_LOADING' +); + +export const setSelectedIndexPatterns = actionCreator<{ + id: SourcererScopeName; + selectedPatterns: string[]; + eventType?: TimelineEventsType; +}>('SET_SELECTED_INDEX_PATTERNS'); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/index.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/index.ts new file mode 100644 index 0000000000000..551c7d8e3efbc --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/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; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as sourcererActions from './actions'; +import * as sourcererModel from './model'; +import * as sourcererSelectors from './selectors'; + +export { sourcererActions, sourcererModel, sourcererSelectors }; +export * from './reducer'; diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts new file mode 100644 index 0000000000000..93f7ff95dfb00 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/model.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns'; +import { DocValueFields } from '../../../../common/search_strategy/common'; +import { + BrowserFields, + EMPTY_BROWSER_FIELDS, + EMPTY_DOCVALUE_FIELD, + EMPTY_INDEX_PATTERN, +} from '../../../../common/search_strategy/index_fields'; + +export type ErrorModel = Error[]; + +export enum SourcererScopeName { + default = 'default', + detections = 'detections', + timeline = 'timeline', +} + +export interface ManageScope { + browserFields: BrowserFields; + docValueFields: DocValueFields[]; + errorMessage: string | null; + id: SourcererScopeName; + indexPattern: IIndexPattern; + indicesExist: boolean | undefined | null; + loading: boolean; + selectedPatterns: string[]; +} + +export interface ManageScopeInit extends Partial { + id: SourcererScopeName; +} + +export type SourcererScopeById = { + [id in SourcererScopeName]: ManageScope; +}; + +export type KibanaIndexPatterns = Array<{ id: string; title: string }>; + +// ManageSourcerer +export interface SourcererModel { + kibanaIndexPatterns: KibanaIndexPatterns; + configIndexPatterns: string[]; + signalIndexName: string | null; + sourcererScopes: SourcererScopeById; +} + +export const initSourcererScope = { + browserFields: EMPTY_BROWSER_FIELDS, + docValueFields: EMPTY_DOCVALUE_FIELD, + errorMessage: null, + indexPattern: EMPTY_INDEX_PATTERN, + indicesExist: true, + loading: true, + selectedPatterns: [], +}; + +export const initialSourcererState: SourcererModel = { + kibanaIndexPatterns: [], + configIndexPatterns: [], + signalIndexName: null, + sourcererScopes: { + [SourcererScopeName.default]: { + ...initSourcererScope, + id: SourcererScopeName.default, + }, + [SourcererScopeName.detections]: { + ...initSourcererScope, + id: SourcererScopeName.detections, + }, + [SourcererScopeName.timeline]: { + ...initSourcererScope, + id: SourcererScopeName.timeline, + }, + }, +}; + +export type FSourcererScopePatterns = { + [id in SourcererScopeName]: string[]; +}; +export type SourcererScopePatterns = Partial; diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts new file mode 100644 index 0000000000000..b65d4d6338e50 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/reducer.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import isEmpty from 'lodash/isEmpty'; +import { reducerWithInitialState } from 'typescript-fsa-reducers'; + +import { + setIndexPatternsList, + setSourcererScopeLoading, + setSelectedIndexPatterns, + setSignalIndexName, + setSource, +} from './actions'; +import { initialSourcererState, SourcererModel, SourcererScopeName } from './model'; + +export type SourcererState = SourcererModel; + +export const sourcererReducer = reducerWithInitialState(initialSourcererState) + .case(setIndexPatternsList, (state, { kibanaIndexPatterns, configIndexPatterns }) => ({ + ...state, + kibanaIndexPatterns, + configIndexPatterns, + })) + .case(setSignalIndexName, (state, { signalIndexName }) => ({ + ...state, + signalIndexName, + })) + .case(setSourcererScopeLoading, (state, { id, loading }) => ({ + ...state, + sourcererScopes: { + ...state.sourcererScopes, + [id]: { + ...state.sourcererScopes[id], + loading, + }, + }, + })) + .case(setSelectedIndexPatterns, (state, { id, selectedPatterns, eventType }) => { + const kibanaIndexPatterns = state.kibanaIndexPatterns.map((kip) => kip.title); + const newSelectedPatterns = selectedPatterns.filter( + (sp) => + state.configIndexPatterns.includes(sp) || + kibanaIndexPatterns.includes(sp) || + (!isEmpty(state.signalIndexName) && state.signalIndexName === sp) + ); + let defaultIndexPatterns = state.configIndexPatterns; + if (id === SourcererScopeName.timeline && isEmpty(newSelectedPatterns)) { + if (eventType === 'all' && !isEmpty(state.signalIndexName)) { + defaultIndexPatterns = [...state.configIndexPatterns, state.signalIndexName ?? '']; + } else if (eventType === 'raw') { + defaultIndexPatterns = state.configIndexPatterns; + } else if ( + !isEmpty(state.signalIndexName) && + (eventType === 'signal' || eventType === 'alert') + ) { + defaultIndexPatterns = [state.signalIndexName ?? '']; + } + } else if (id === SourcererScopeName.detections && isEmpty(newSelectedPatterns)) { + defaultIndexPatterns = [state.signalIndexName ?? '']; + } + return { + ...state, + sourcererScopes: { + ...state.sourcererScopes, + [id]: { + ...state.sourcererScopes[id], + selectedPatterns: isEmpty(newSelectedPatterns) + ? defaultIndexPatterns + : newSelectedPatterns, + }, + }, + }; + }) + .case(setSource, (state, { id, payload }) => { + const { ...sourcererScopes } = payload; + return { + ...state, + sourcererScopes: { + ...state.sourcererScopes, + [id]: { + ...state.sourcererScopes[id], + ...sourcererScopes, + ...(state.sourcererScopes[id].selectedPatterns.length === 0 + ? { selectedPatterns: state.configIndexPatterns } + : {}), + }, + }, + }; + }) + .build(); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts new file mode 100644 index 0000000000000..ca9ea26ba5bac --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createSelector } from 'reselect'; +import { State } from '../types'; +import { SourcererScopeById, KibanaIndexPatterns, SourcererScopeName, ManageScope } from './model'; + +export const sourcererKibanaIndexPatternsSelector = ({ sourcerer }: State): KibanaIndexPatterns => + sourcerer.kibanaIndexPatterns; + +export const sourcererSignalIndexNameSelector = ({ sourcerer }: State): string | null => + sourcerer.signalIndexName; + +export const sourcererConfigIndexPatternsSelector = ({ sourcerer }: State): string[] => + sourcerer.configIndexPatterns; + +export const sourcererScopesSelector = ({ sourcerer }: State): SourcererScopeById => + sourcerer.sourcererScopes; + +export const scopesSelector = () => createSelector(sourcererScopesSelector, (scopes) => scopes); + +export const kibanaIndexPatternsSelector = () => + createSelector( + sourcererKibanaIndexPatternsSelector, + (kibanaIndexPatterns) => kibanaIndexPatterns + ); + +export const signalIndexNameSelector = () => + createSelector(sourcererSignalIndexNameSelector, (signalIndexName) => signalIndexName); + +export const configIndexPatternsSelector = () => + createSelector( + sourcererConfigIndexPatternsSelector, + (configIndexPatterns) => configIndexPatterns + ); + +export const getIndexNamesSelectedSelector = () => { + const getScopesSelector = scopesSelector(); + const getConfigIndexPatternsSelector = configIndexPatternsSelector(); + + const mapStateToProps = (state: State, scopeId: SourcererScopeName): string[] => { + const scope = getScopesSelector(state)[scopeId]; + const configIndexPatterns = getConfigIndexPatternsSelector(state); + + return scope.selectedPatterns.length === 0 ? configIndexPatterns : scope.selectedPatterns; + }; + + return mapStateToProps; +}; + +export const getAllExistingIndexNamesSelector = () => { + const getSignalIndexNameSelector = signalIndexNameSelector(); + const getConfigIndexPatternsSelector = configIndexPatternsSelector(); + + const mapStateToProps = (state: State): string[] => { + const signalIndexName = getSignalIndexNameSelector(state); + const configIndexPatterns = getConfigIndexPatternsSelector(state); + + return signalIndexName != null + ? [...configIndexPatterns, signalIndexName] + : configIndexPatterns; + }; + + return mapStateToProps; +}; + +export const defaultIndexNamesSelector = () => { + const getScopesSelector = scopesSelector(); + const getConfigIndexPatternsSelector = configIndexPatternsSelector(); + + const mapStateToProps = (state: State, scopeId: SourcererScopeName): string[] => { + const scope = getScopesSelector(state)[scopeId]; + const configIndexPatterns = getConfigIndexPatternsSelector(state); + + return scope.selectedPatterns.length === 0 ? configIndexPatterns : scope.selectedPatterns; + }; + + return mapStateToProps; +}; + +export const getSourcererScopeSelector = () => { + const getScopesSelector = scopesSelector(); + + const mapStateToProps = (state: State, scopeId: SourcererScopeName): ManageScope => + getScopesSelector(state)[scopeId]; + + return mapStateToProps; +}; diff --git a/x-pack/plugins/security_solution/public/common/store/types.ts b/x-pack/plugins/security_solution/public/common/store/types.ts index 91d92e4758c4a..6903567c752bc 100644 --- a/x-pack/plugins/security_solution/public/common/store/types.ts +++ b/x-pack/plugins/security_solution/public/common/store/types.ts @@ -12,6 +12,7 @@ import { AppAction } from './actions'; import { Immutable } from '../../../common/endpoint/types'; import { AppState } from './app/reducer'; import { InputsState } from './inputs/reducer'; +import { SourcererState } from './sourcerer/reducer'; import { HostsPluginState } from '../../hosts/store'; import { DragAndDropState } from './drag_and_drop/reducer'; import { TimelinePluginState } from '../../timelines/store/timeline'; @@ -25,6 +26,7 @@ export type StoreState = HostsPluginState & app: AppState; dragAndDrop: DragAndDropState; inputs: InputsState; + sourcerer: SourcererState; }; /** * The redux `State` type for the Security App. diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 678aaf06e50e4..e3440f4158513 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -197,6 +197,7 @@ describe('alert actions', () => { highlightedDropAndProviderId: '', historyIds: [], id: '', + indexNames: [], isFavorite: false, isLive: false, isLoading: false, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index 640726bb2e7c8..7f98d3b2f71de 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -279,6 +279,7 @@ export const sendAlertToTimelineAction = async ({ ...getThresholdAggregationDataProvider(ecsData, nonEcsData), ], id: TimelineId.active, + indexNames: [], dateRange: { start: from, end: to, @@ -329,6 +330,7 @@ export const sendAlertToTimelineAction = async ({ }, ], id: TimelineId.active, + indexNames: [], dateRange: { start: from, end: to, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx index be24957602037..6724d3a83d617 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx @@ -22,7 +22,6 @@ describe('AlertsTableComponent', () => { hasIndexWrite from={'2020-07-07T08:20:18.966Z'} loading - signalsIndex="index" to={'2020-07-08T08:20:18.966Z'} globalQuery={{ query: 'query', diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 0416b3d2a459f..d66d37a020040 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -13,7 +13,6 @@ import { Status } from '../../../../common/detection_engine/schemas/common/schem import { Filter, esQuery } from '../../../../../../../src/plugins/data/public'; import { TimelineIdLiteral } from '../../../../common/types/timeline'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns'; import { StatefulEventsViewer } from '../../../common/components/events_viewer'; import { HeaderSection } from '../../../common/components/header_section'; import { combineQueries } from '../../../timelines/components/timeline/helpers'; @@ -45,6 +44,8 @@ import { displaySuccessToast, displayErrorToast, } from '../../../common/components/toasters'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; +import { useSourcererScope } from '../../../common/containers/sourcerer'; interface OwnProps { timelineId: TimelineIdLiteral; @@ -55,7 +56,6 @@ interface OwnProps { loading: boolean; showBuildingBlockAlerts: boolean; onShowBuildingBlockAlertsChanged: (showBuildingBlockAlerts: boolean) => void; - signalsIndex: string; to: string; } @@ -80,19 +80,20 @@ export const AlertsTableComponent: React.FC = ({ setEventsLoading, showBuildingBlockAlerts, onShowBuildingBlockAlertsChanged, - signalsIndex, to, }) => { const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); - const [{ browserFields, indexPatterns, isLoading: indexPatternsLoading }] = useFetchIndexPatterns( - signalsIndex !== '' ? [signalsIndex] : [], - 'alerts_table' - ); + const { + browserFields, + indexPattern: indexPatterns, + loading: indexPatternsLoading, + selectedPatterns, + } = useSourcererScope(SourcererScopeName.detections); const kibana = useKibana(); const [, dispatchToaster] = useStateToaster(); const { addWarning } = useAppToasts(); - const { initializeTimeline, setSelectAll, setIndexToAdd } = useManageTimeline(); + const { initializeTimeline, setSelectAll } = useManageTimeline(); const getGlobalQuery = useCallback( (customFilters: Filter[]) => { @@ -284,7 +285,6 @@ export const AlertsTableComponent: React.FC = ({ ] ); - const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]); const defaultFiltersMemo = useMemo(() => { if (isEmpty(defaultFilters)) { return buildAlertStatusFilter(filterGroup); @@ -301,7 +301,6 @@ export const AlertsTableComponent: React.FC = ({ filterManager, footerText: i18n.TOTAL_COUNT_OF_ALERTS, id: timelineId, - indexToAdd: defaultIndices, loadingText: i18n.LOADING_ALERTS, selectAll: false, queryFields: requiredFieldsForActions, @@ -310,16 +309,12 @@ export const AlertsTableComponent: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useEffect(() => { - setIndexToAdd({ id: timelineId, indexToAdd: defaultIndices }); - }, [timelineId, defaultIndices, setIndexToAdd]); - const headerFilterGroup = useMemo( () => , [onFilterGroupChangedCallback] ); - if (loading || indexPatternsLoading || isEmpty(signalsIndex)) { + if (loading || indexPatternsLoading || isEmpty(selectedPatterns)) { return ( @@ -330,12 +325,12 @@ export const AlertsTableComponent: React.FC = ({ return ( diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 4559e44b8c3c5..82fed152ea66d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -109,7 +109,7 @@ const AlertContextMenuComponent: React.FC = ({ const closeAddExceptionModal = useCallback(() => { setShouldShowAddExceptionModal(false); setAddExceptionModalState(addExceptionModalInitialState); - }, [setShouldShowAddExceptionModal, setAddExceptionModalState]); + }, []); const onAddExceptionCancel = useCallback(() => { closeAddExceptionModal(); @@ -305,33 +305,6 @@ const AlertContextMenuComponent: React.FC = ({ [setShouldShowAddExceptionModal, setAddExceptionModalState] ); - const AddExceptionModal = useCallback( - () => - shouldShowAddExceptionModal === true && addExceptionModalState.alertData !== null ? ( - - ) : null, - [ - shouldShowAddExceptionModal, - addExceptionModalState.alertData, - addExceptionModalState.ruleName, - addExceptionModalState.ruleId, - addExceptionModalState.ruleIndices, - addExceptionModalState.exceptionListType, - onAddExceptionCancel, - onAddExceptionConfirm, - alertStatus, - ] - ); - const button = ( = ({ - + {shouldShowAddExceptionModal === true && addExceptionModalState.alertData !== null && ( + + )} ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx index 4ab5fa5e6012f..f4649b016f67c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx @@ -15,7 +15,6 @@ import { useApolloClient } from '../../../../common/utils/apollo_context'; import { sendAlertToTimelineAction } from '../actions'; import { dispatchUpdateTimeline } from '../../../../timelines/components/open_timeline/helpers'; import { ActionIconItem } from '../../../../timelines/components/timeline/body/actions/action_icon_item'; - import { CreateTimelineProps } from '../types'; import { ACTION_INVESTIGATE_IN_TIMELINE, @@ -49,6 +48,8 @@ const InvestigateInTimelineActionComponent: React.FC = (props) => ( - + ); DetectionEngineHeaderPageComponent.defaultProps = { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx index cb25785eaa5b2..4312be0b46990 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx @@ -8,8 +8,9 @@ import { mount, shallow } from 'enzyme'; import { ThemeProvider } from 'styled-components'; import euiDarkVars from '@elastic/eui/dist/eui_theme_light.json'; +import { stubIndexPattern } from 'src/plugins/data/common/index_patterns/index_pattern.stub'; import { StepAboutRule } from '.'; - +import { useFetchIndex } from '../../../../common/containers/source'; import { mockAboutStepRule } from '../../../pages/detection_engine/rules/all/__mocks__/mock'; import { StepRuleDescription } from '../description_step'; import { stepAboutDefaultValue } from './default_value'; @@ -20,6 +21,7 @@ import { } from '../../../pages/detection_engine/rules/types'; import { fillEmptySeverityMappings } from '../../../pages/detection_engine/rules/helpers'; +jest.mock('../../../../common/containers/source'); const theme = () => ({ eui: euiDarkVars, darkMode: true }); /* eslint-disable no-console */ @@ -44,6 +46,12 @@ describe('StepAboutRuleComponent', () => { beforeEach(() => { formHook = null; + (useFetchIndex as jest.Mock).mockImplementation(() => [ + false, + { + indexPatterns: stubIndexPattern, + }, + ]); }); test('it renders StepRuleDescription if isReadOnlyView is true and "name" property exists', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 66f95f5ce15d2..90b70e53a459e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -39,8 +39,8 @@ import { NextStep } from '../next_step'; import { MarkdownEditorForm } from '../../../../common/components/markdown_editor/eui_form'; import { SeverityField } from '../severity_mapping'; import { RiskScoreField } from '../risk_score_mapping'; -import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules'; import { AutocompleteField } from '../autocomplete_field'; +import { useFetchIndex } from '../../../../common/containers/source'; const CommonUseField = getUseField({ component: Field }); @@ -74,10 +74,8 @@ const StepAboutRuleComponent: FC = ({ }) => { const initialState = defaultValues ?? stepAboutDefaultValue; const [severityValue, setSeverityValue] = useState(initialState.severity.value); - const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( - defineRuleData?.index ?? [], - RuleStep.aboutRule - ); + const [indexPatternLoading, { indexPatterns }] = useFetchIndex(defineRuleData?.index ?? []); + const canUseExceptions = defineRuleData?.ruleType && !isMlRule(defineRuleData.ruleType) && diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 7846f0c406668..99999ddbf1976 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -14,7 +14,6 @@ import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timelin import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions'; import { hasMlLicense } from '../../../../../common/machine_learning/has_ml_license'; -import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules'; import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities'; import { useUiSetting$ } from '../../../../common/lib/kibana'; import { @@ -48,6 +47,7 @@ import { schema } from './schema'; import * as i18n from './translations'; import { isEqlRule, isThresholdRule } from '../../../../../common/detection_engine/utils'; import { EqlQueryBar } from '../eql_query_bar'; +import { useFetchIndex } from '../../../../common/containers/source'; const CommonUseField = getUseField({ component: Field }); @@ -125,10 +125,7 @@ const StepDefineRuleComponent: FC = ({ }) as unknown) as [Partial]; const index = formIndex || initialState.index; const ruleType = formRuleType || initialState.ruleType; - const [{ browserFields, indexPatterns, isLoading: indexPatternsLoading }] = useFetchIndexPatterns( - index, - RuleStep.defineRule - ); + const [indexPatternsLoading, { browserFields, indexPatterns }] = useFetchIndex(index); // reset form when rule type changes useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx index e1a29c3575d95..00e108ffb89b6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx @@ -169,22 +169,19 @@ export const useUserInfo = (): State => { if (loading !== privilegeLoading || indexNameLoading) { dispatch({ type: 'updateLoading', loading: privilegeLoading || indexNameLoading }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loading, privilegeLoading, indexNameLoading]); + }, [dispatch, loading, privilegeLoading, indexNameLoading]); useEffect(() => { if (!loading && hasIndexManage !== hasApiIndexManage && hasApiIndexManage != null) { dispatch({ type: 'updateHasIndexManage', hasIndexManage: hasApiIndexManage }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loading, hasIndexManage, hasApiIndexManage]); + }, [dispatch, loading, hasIndexManage, hasApiIndexManage]); useEffect(() => { if (!loading && hasIndexWrite !== hasApiIndexWrite && hasApiIndexWrite != null) { dispatch({ type: 'updateHasIndexWrite', hasIndexWrite: hasApiIndexWrite }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loading, hasIndexWrite, hasApiIndexWrite]); + }, [dispatch, loading, hasIndexWrite, hasApiIndexWrite]); useEffect(() => { if ( @@ -194,36 +191,31 @@ export const useUserInfo = (): State => { ) { dispatch({ type: 'updateIsSignalIndexExists', isSignalIndexExists: isApiSignalIndexExists }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loading, isSignalIndexExists, isApiSignalIndexExists]); + }, [dispatch, loading, isSignalIndexExists, isApiSignalIndexExists]); useEffect(() => { if (!loading && isAuthenticated !== isApiAuthenticated && isApiAuthenticated != null) { dispatch({ type: 'updateIsAuthenticated', isAuthenticated: isApiAuthenticated }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loading, isAuthenticated, isApiAuthenticated]); + }, [dispatch, loading, isAuthenticated, isApiAuthenticated]); useEffect(() => { if (!loading && hasEncryptionKey !== isApiEncryptionKey && isApiEncryptionKey != null) { dispatch({ type: 'updateHasEncryptionKey', hasEncryptionKey: isApiEncryptionKey }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loading, hasEncryptionKey, isApiEncryptionKey]); + }, [dispatch, loading, hasEncryptionKey, isApiEncryptionKey]); useEffect(() => { if (!loading && canUserCRUD !== capabilitiesCanUserCRUD && capabilitiesCanUserCRUD != null) { dispatch({ type: 'updateCanUserCRUD', canUserCRUD: capabilitiesCanUserCRUD }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loading, canUserCRUD, capabilitiesCanUserCRUD]); + }, [dispatch, loading, canUserCRUD, capabilitiesCanUserCRUD]); useEffect(() => { if (!loading && signalIndexName !== apiSignalIndexName && apiSignalIndexName != null) { dispatch({ type: 'updateSignalIndexName', signalIndexName: apiSignalIndexName }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loading, signalIndexName, apiSignalIndexName]); + }, [dispatch, loading, signalIndexName, apiSignalIndexName]); useEffect(() => { if ( diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx deleted file mode 100644 index d36c19a6a35c6..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.test.tsx +++ /dev/null @@ -1,475 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { renderHook, act } from '@testing-library/react-hooks'; - -import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; -import { useApolloClient } from '../../../../common/utils/apollo_context'; -import { mocksSource } from '../../../../common/containers/source/mock'; - -import { useFetchIndexPatterns, Return } from './fetch_index_patterns'; - -const mockUseApolloClient = useApolloClient as jest.Mock; -jest.mock('../../../../common/utils/apollo_context'); - -describe('useFetchIndexPatterns', () => { - beforeEach(() => { - mockUseApolloClient.mockClear(); - }); - test('happy path', async () => { - await act(async () => { - mockUseApolloClient.mockImplementation(() => ({ - query: () => Promise.resolve(mocksSource[0].result), - })); - const { result, waitForNextUpdate } = renderHook(() => - useFetchIndexPatterns(DEFAULT_INDEX_PATTERN) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(result.current).toEqual([ - { - browserFields: { - base: { - fields: { - '@timestamp': { - category: 'base', - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - example: '2016-05-23T08:05:34.853Z', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: '@timestamp', - searchable: true, - type: 'date', - aggregatable: true, - }, - }, - }, - agent: { - fields: { - 'agent.ephemeral_id': { - category: 'agent', - description: - 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.ephemeral_id', - searchable: true, - type: 'string', - aggregatable: true, - }, - 'agent.hostname': { - category: 'agent', - description: null, - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.hostname', - searchable: true, - type: 'string', - aggregatable: true, - }, - 'agent.id': { - category: 'agent', - description: - 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', - example: '8a4f500d', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.id', - searchable: true, - type: 'string', - aggregatable: true, - }, - 'agent.name': { - category: 'agent', - description: - 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', - example: 'foo', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.name', - searchable: true, - type: 'string', - aggregatable: true, - }, - }, - }, - auditd: { - fields: { - 'auditd.data.a0': { - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a0', - searchable: true, - type: 'string', - aggregatable: true, - }, - 'auditd.data.a1': { - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a1', - searchable: true, - type: 'string', - aggregatable: true, - }, - 'auditd.data.a2': { - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a2', - searchable: true, - type: 'string', - aggregatable: true, - }, - }, - }, - client: { - fields: { - 'client.address': { - category: 'client', - description: - 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.address', - searchable: true, - type: 'string', - aggregatable: true, - }, - 'client.bytes': { - category: 'client', - description: 'Bytes sent from the client to the server.', - example: '184', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.bytes', - searchable: true, - type: 'number', - aggregatable: true, - }, - 'client.domain': { - category: 'client', - description: 'Client domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.domain', - searchable: true, - type: 'string', - aggregatable: true, - }, - 'client.geo.country_iso_code': { - category: 'client', - description: 'Country ISO code.', - example: 'CA', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.geo.country_iso_code', - searchable: true, - type: 'string', - aggregatable: true, - }, - }, - }, - cloud: { - fields: { - 'cloud.account.id': { - category: 'cloud', - description: - 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', - example: '666777888999', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.account.id', - searchable: true, - type: 'string', - aggregatable: true, - }, - 'cloud.availability_zone': { - category: 'cloud', - description: 'Availability zone in which this host is running.', - example: 'us-east-1c', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.availability_zone', - searchable: true, - type: 'string', - aggregatable: true, - }, - }, - }, - container: { - fields: { - 'container.id': { - category: 'container', - description: 'Unique container id.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.id', - searchable: true, - type: 'string', - aggregatable: true, - }, - 'container.image.name': { - category: 'container', - description: 'Name of the image the container was built on.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.image.name', - searchable: true, - type: 'string', - aggregatable: true, - }, - 'container.image.tag': { - category: 'container', - description: 'Container image tag.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.image.tag', - searchable: true, - type: 'string', - aggregatable: true, - }, - }, - }, - destination: { - fields: { - 'destination.address': { - category: 'destination', - description: - 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.address', - searchable: true, - type: 'string', - aggregatable: true, - }, - 'destination.bytes': { - category: 'destination', - description: 'Bytes sent from the destination to the source.', - example: '184', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.bytes', - searchable: true, - type: 'number', - aggregatable: true, - }, - 'destination.domain': { - category: 'destination', - description: 'Destination domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.domain', - searchable: true, - type: 'string', - aggregatable: true, - }, - 'destination.ip': { - aggregatable: true, - category: 'destination', - description: - 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.ip', - searchable: true, - type: 'ip', - }, - 'destination.port': { - aggregatable: true, - category: 'destination', - description: 'Port of the destination.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.port', - searchable: true, - type: 'long', - }, - }, - }, - source: { - fields: { - 'source.ip': { - aggregatable: true, - category: 'source', - description: - 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'source.ip', - searchable: true, - type: 'ip', - }, - 'source.port': { - aggregatable: true, - category: 'source', - description: 'Port of the source.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'source.port', - searchable: true, - type: 'long', - }, - }, - }, - event: { - fields: { - 'event.end': { - aggregatable: true, - category: 'event', - description: - 'event.end contains the date when the event ended or when the activity was last observed.', - example: null, - format: '', - indexes: [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', - ], - name: 'event.end', - searchable: true, - type: 'date', - }, - }, - }, - }, - isLoading: false, - indices: [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', - ], - indicesExists: true, - docValueFields: [ - { - field: '@timestamp', - format: 'date_time', - }, - { - field: 'event.end', - format: 'date_time', - }, - ], - indexPatterns: { - fields: [ - { name: '@timestamp', searchable: true, type: 'date', aggregatable: true }, - { name: 'agent.ephemeral_id', searchable: true, type: 'string', aggregatable: true }, - { name: 'agent.hostname', searchable: true, type: 'string', aggregatable: true }, - { name: 'agent.id', searchable: true, type: 'string', aggregatable: true }, - { name: 'agent.name', searchable: true, type: 'string', aggregatable: true }, - { name: 'auditd.data.a0', searchable: true, type: 'string', aggregatable: true }, - { name: 'auditd.data.a1', searchable: true, type: 'string', aggregatable: true }, - { name: 'auditd.data.a2', searchable: true, type: 'string', aggregatable: true }, - { name: 'client.address', searchable: true, type: 'string', aggregatable: true }, - { name: 'client.bytes', searchable: true, type: 'number', aggregatable: true }, - { name: 'client.domain', searchable: true, type: 'string', aggregatable: true }, - { - name: 'client.geo.country_iso_code', - searchable: true, - type: 'string', - aggregatable: true, - }, - { name: 'cloud.account.id', searchable: true, type: 'string', aggregatable: true }, - { - name: 'cloud.availability_zone', - searchable: true, - type: 'string', - aggregatable: true, - }, - { name: 'container.id', searchable: true, type: 'string', aggregatable: true }, - { - name: 'container.image.name', - searchable: true, - type: 'string', - aggregatable: true, - }, - { name: 'container.image.tag', searchable: true, type: 'string', aggregatable: true }, - { name: 'destination.address', searchable: true, type: 'string', aggregatable: true }, - { name: 'destination.bytes', searchable: true, type: 'number', aggregatable: true }, - { name: 'destination.domain', searchable: true, type: 'string', aggregatable: true }, - { name: 'destination.ip', searchable: true, type: 'ip', aggregatable: true }, - { name: 'destination.port', searchable: true, type: 'long', aggregatable: true }, - { name: 'source.ip', searchable: true, type: 'ip', aggregatable: true }, - { name: 'source.port', searchable: true, type: 'long', aggregatable: true }, - { name: 'event.end', searchable: true, type: 'date', aggregatable: true }, - ], - title: - 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*', - }, - }, - result.current[1], - ]); - }); - }); - - test('unhappy path', async () => { - await act(async () => { - mockUseApolloClient.mockImplementation(() => ({ - query: () => Promise.reject(new Error('Something went wrong')), - })); - const { result, waitForNextUpdate } = renderHook(() => - useFetchIndexPatterns(DEFAULT_INDEX_PATTERN) - ); - - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(result.current).toEqual([ - { - browserFields: {}, - docValueFields: [], - indexPatterns: { - fields: [], - title: '', - }, - indices: [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'logs-*', - 'packetbeat-*', - 'winlogbeat-*', - ], - indicesExists: false, - isLoading: false, - }, - result.current[1], - ]); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx deleted file mode 100644 index 82c9292af7451..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/fetch_index_patterns.tsx +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty, get } from 'lodash/fp'; -import { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import deepEqual from 'fast-deep-equal'; - -import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; -import { - BrowserFields, - getBrowserFields, - getDocValueFields, - getIndexFields, - sourceQuery, - DocValueFields, -} from '../../../../common/containers/source'; -import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; -import { SourceQuery } from '../../../../graphql/types'; -import { useApolloClient } from '../../../../common/utils/apollo_context'; - -import * as i18n from './translations'; - -interface FetchIndexPatternReturn { - browserFields: BrowserFields; - docValueFields: DocValueFields[]; - isLoading: boolean; - indices: string[]; - indicesExists: boolean; - indexPatterns: IIndexPattern; -} - -export type Return = [FetchIndexPatternReturn, Dispatch>]; - -const DEFAULT_BROWSER_FIELDS = {}; -const DEFAULT_INDEX_PATTERNS = { fields: [], title: '' }; -const DEFAULT_DOC_VALUE_FIELDS: DocValueFields[] = []; - -// Fun fact: When using this hook multiple times within a component (e.g. add_exception_modal & edit_exception_modal), -// the apolloClient will perform queryDeduplication and prevent the first query from executing. A deep compare is not -// performed on `indices`, so another field must be passed to circumvent this. -// For details, see https://github.com/apollographql/react-apollo/issues/2202 -export const useFetchIndexPatterns = ( - defaultIndices: string[] = [], - queryDeduplication?: string -): Return => { - const apolloClient = useApolloClient(); - const [indices, setIndices] = useState(defaultIndices); - - const [state, setState] = useState({ - browserFields: DEFAULT_BROWSER_FIELDS, - docValueFields: DEFAULT_DOC_VALUE_FIELDS, - indices: defaultIndices, - indicesExists: false, - indexPatterns: DEFAULT_INDEX_PATTERNS, - isLoading: false, - }); - - const [, dispatchToaster] = useStateToaster(); - - useEffect(() => { - if (!deepEqual(defaultIndices, indices)) { - setIndices(defaultIndices); - setState((prevState) => ({ ...prevState, indices: defaultIndices })); - } - }, [defaultIndices, indices]); - - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - - async function fetchIndexPatterns() { - if (apolloClient && !isEmpty(indices)) { - setState((prevState) => ({ ...prevState, isLoading: true })); - apolloClient - .query({ - query: sourceQuery, - fetchPolicy: 'cache-first', - variables: { - sourceId: 'default', - defaultIndex: indices, - ...(queryDeduplication != null ? { queryDeduplication } : {}), - }, - context: { - fetchOptions: { - signal: abortCtrl.signal, - }, - }, - }) - .then( - (result) => { - if (isSubscribed) { - setState({ - browserFields: getBrowserFields( - indices.join(), - get('data.source.status.indexFields', result) - ), - docValueFields: getDocValueFields( - indices.join(), - get('data.source.status.indexFields', result) - ), - indices, - isLoading: false, - indicesExists: get('data.source.status.indicesExist', result), - indexPatterns: getIndexFields( - indices.join(), - get('data.source.status.indexFields', result) - ), - }); - } - }, - (error) => { - if (isSubscribed) { - setState((prevState) => ({ ...prevState, isLoading: false })); - errorToToaster({ title: i18n.RULE_ADD_FAILURE, error, dispatchToaster }); - } - } - ); - } - } - fetchIndexPatterns(); - return () => { - isSubscribed = false; - abortCtrl.abort(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [indices]); - - return [state, setIndices]; -}; 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 a40ab2e487851..930391261ac87 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 @@ -5,7 +5,6 @@ */ export * from './api'; -export * from './fetch_index_patterns'; export * from './use_update_rule'; export * from './use_create_rule'; export * from './types'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index 8c21f6a1e8cb7..a5d21d2847586 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -20,7 +20,7 @@ import { import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; import { DetectionEnginePageComponent } from './detection_engine'; import { useUserData } from '../../components/user_info'; -import { useWithSource } from '../../../common/containers/source'; +import { useSourcererScope } from '../../../common/containers/sourcerer'; import { createStore, State } from '../../../common/store'; import { mockHistory, Router } from '../../../cases/components/__mock__/router'; @@ -34,7 +34,7 @@ jest.mock('../../../common/components/query_bar', () => ({ })); jest.mock('../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../components/user_info'); -jest.mock('../../../common/containers/source'); +jest.mock('../../../common/containers/sourcerer'); jest.mock('../../../common/components/link_to'); jest.mock('../../../common/containers/use_global_time', () => ({ useGlobalTime: jest.fn().mockReturnValue({ @@ -74,7 +74,7 @@ describe('DetectionEnginePageComponent', () => { beforeAll(() => { (useParams as jest.Mock).mockReturnValue({}); (useUserData as jest.Mock).mockReturnValue([{}]); - (useWithSource as jest.Mock).mockReturnValue({ + (useSourcererScope as jest.Mock).mockReturnValue({ indicesExist: true, indexPattern: {}, }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index 3a3854f145db3..b39cd37521602 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -13,7 +13,6 @@ import { useHistory } from 'react-router-dom'; import { SecurityPageName } from '../../../app/types'; import { TimelineId } from '../../../../common/types/timeline'; import { useGlobalTime } from '../../../common/containers/use_global_time'; -import { useWithSource } from '../../../common/containers/source'; import { UpdateDateRange } from '../../../common/components/charts/common'; import { FiltersGlobal } from '../../../common/components/filters_global'; import { getRulesUrl } from '../../../common/components/link_to/redirect_to_detection_engine'; @@ -46,6 +45,8 @@ import { timelineSelectors } from '../../../timelines/store/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { TimelineModel } from '../../../timelines/store/timeline/model'; import { buildShowBuildingBlockFilter } from '../../components/alerts_table/default_config'; +import { useSourcererScope } from '../../../common/containers/sourcerer'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; export const DetectionEnginePageComponent: React.FC = ({ filters, @@ -117,10 +118,7 @@ export const DetectionEnginePageComponent: React.FC = ({ [setShowBuildingBlockAlerts] ); - const indexToAdd = useMemo(() => (signalIndexName == null ? [] : [signalIndexName]), [ - signalIndexName, - ]); - const { indicesExist, indexPattern } = useWithSource('default', indexToAdd); + const { indicesExist, indexPattern } = useSourcererScope(SourcererScopeName.detections); if (isUserAuthenticated != null && !isUserAuthenticated && !loading) { return ( @@ -202,7 +200,6 @@ export const DetectionEnginePageComponent: React.FC = ({ defaultFilters={alertsTableDefaultFilters} showBuildingBlockAlerts={showBuildingBlockAlerts} onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChangedCallback} - signalsIndex={signalIndexName ?? ''} to={to} /> 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 f8f9da78b2a06..22c3c43fb2356 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,7 @@ import { RuleDetailsPageComponent } from './index'; import { createStore, State } from '../../../../../common/store'; import { setAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/actions'; import { useUserData } from '../../../../components/user_info'; -import { useWithSource } from '../../../../../common/containers/source'; +import { useSourcererScope } from '../../../../../common/containers/sourcerer'; import { useParams } from 'react-router-dom'; import { mockHistory, Router } from '../../../../../cases/components/__mock__/router'; @@ -35,7 +35,7 @@ jest.mock('../../../../../common/components/query_bar', () => ({ jest.mock('../../../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); -jest.mock('../../../../../common/containers/source'); +jest.mock('../../../../../common/containers/sourcerer'); jest.mock('../../../../../common/containers/use_global_time', () => ({ useGlobalTime: jest.fn().mockReturnValue({ from: '2020-07-07T08:20:18.966Z', @@ -71,7 +71,7 @@ describe('RuleDetailsPageComponent', () => { beforeAll(() => { (useUserData as jest.Mock).mockReturnValue([{}]); (useParams as jest.Mock).mockReturnValue({}); - (useWithSource as jest.Mock).mockReturnValue({ + (useSourcererScope as jest.Mock).mockReturnValue({ indicesExist: true, indexPattern: {}, }); 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 68799f46eee57..4816358e06226 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 @@ -36,10 +36,7 @@ import { SiemSearchBar } from '../../../../../common/components/search_bar'; import { WrapperPage } from '../../../../../common/components/wrapper_page'; import { Rule } from '../../../../containers/detection_engine/rules'; import { useListsConfig } from '../../../../containers/detection_engine/lists/use_lists_config'; - -import { useWithSource } from '../../../../../common/containers/source'; import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; - import { StepAboutRuleToggleDetails } from '../../../../components/rules/step_about_rule_details'; import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page'; import { AlertsHistogramPanel } from '../../../../components/alerts_histogram_panel'; @@ -89,6 +86,8 @@ import { showGlobalFilters } from '../../../../../timelines/components/timeline/ import { timelineSelectors } from '../../../../../timelines/store/timeline'; import { timelineDefaults } from '../../../../../timelines/store/timeline/defaults'; import { TimelineModel } from '../../../../../timelines/store/timeline/model'; +import { useSourcererScope } from '../../../../../common/containers/sourcerer'; +import { SourcererScopeName } from '../../../../../common/store/sourcerer/model'; enum RuleDetailTabs { alerts = 'alerts', @@ -265,10 +264,6 @@ export const RuleDetailsPageComponent: FC = ({ [rule, ruleDetailTab] ); - const indexToAdd = useMemo(() => (signalIndexName == null ? [] : [signalIndexName]), [ - signalIndexName, - ]); - const updateDateRangeCallback = useCallback( ({ x }) => { if (!x) { @@ -308,7 +303,7 @@ export const RuleDetailsPageComponent: FC = ({ [setShowBuildingBlockAlerts] ); - const { indicesExist, indexPattern } = useWithSource('default', indexToAdd); + const { indicesExist, indexPattern } = useSourcererScope(SourcererScopeName.detections); const exceptionLists = useMemo((): { lists: ExceptionIdentifiers[]; @@ -500,7 +495,6 @@ export const RuleDetailsPageComponent: FC = ({ loading={loading} showBuildingBlockAlerts={showBuildingBlockAlerts} onShowBuildingBlockAlertsChanged={onShowBuildingBlockAlertsChangedCallback} - signalsIndex={signalIndexName ?? ''} to={to} /> )} diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 9e6a4f21ec64f..568a960f0804e 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -2382,7 +2382,7 @@ "ofType": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "OBJECT", "name": "IndexField", "ofType": null } + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } } } }, @@ -2405,153 +2405,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "IndexField", - "description": "A descriptor of a field in an index", - "fields": [ - { - "name": "category", - "description": "Where the field belong", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "example", - "description": "Example of field's value", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "indexes", - "description": "whether the field's belong to an alias index", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "name", - "description": "The name of the field", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "type", - "description": "The type of the field's values as recognized by Kibana", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "searchable", - "description": "Whether the field's values can be efficiently searched for", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "aggregatable", - "description": "Whether the field's values can be aggregated", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": "Description of the field", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "format", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "esTypes", - "description": "the elastic type as mapped in the index", - "args": [], - "type": { "kind": "SCALAR", "name": "ToStringArrayNoNullable", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "subType", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "ToIFieldSubTypeNonNullable", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "ToStringArrayNoNullable", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "SCALAR", - "name": "ToIFieldSubTypeNonNullable", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, { "kind": "INPUT_OBJECT", "name": "TimerangeInput", @@ -9466,6 +9319,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "indexNames", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "notes", "description": "", @@ -10933,6 +10802,20 @@ }, "defaultValue": null }, + { + "name": "indexNames", + "description": "", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + }, + "defaultValue": null + }, { "name": "title", "description": "", @@ -12214,6 +12097,16 @@ ], "possibleTypes": null }, + { + "kind": "SCALAR", + "name": "ToStringArrayNoNullable", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "EcsEdges", @@ -12548,6 +12441,143 @@ ], "possibleTypes": null }, + { + "kind": "SCALAR", + "name": "ToIFieldSubTypeNonNullable", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "IndexField", + "description": "A descriptor of a field in an index", + "fields": [ + { + "name": "category", + "description": "Where the field belong", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "example", + "description": "Example of field's value", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "indexes", + "description": "whether the field's belong to an alias index", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "The name of the field", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": "The type of the field's values as recognized by Kibana", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "searchable", + "description": "Whether the field's values can be efficiently searched for", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "aggregatable", + "description": "Whether the field's values can be aggregated", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": "Description of the field", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "format", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "esTypes", + "description": "the elastic type as mapped in the index", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArrayNoNullable", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subType", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToIFieldSubTypeNonNullable", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "ENUM", "name": "FlowDirection", diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 1699ac4dd33eb..0bce952912c5c 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -132,6 +132,8 @@ export interface TimelineInput { kqlQuery?: Maybe; + indexNames?: Maybe; + title?: Maybe; templateTimelineId?: Maybe; @@ -413,10 +415,6 @@ export enum FlowDirection { biDirectional = 'biDirectional', } -export type ToStringArrayNoNullable = any; - -export type ToIFieldSubTypeNonNullable = any; - export type ToStringArray = string[]; export type Date = string; @@ -431,6 +429,10 @@ export type ToAny = any; export type EsValue = any; +export type ToStringArrayNoNullable = any; + +export type ToIFieldSubTypeNonNullable = any; + // ==================================================== // Scalars // ==================================================== @@ -589,33 +591,7 @@ export interface SourceStatus { /** Whether the configured alias or wildcard pattern resolve to any auditbeat indices */ indicesExist: boolean; /** The list of fields defined in the index mappings */ - indexFields: IndexField[]; -} - -/** A descriptor of a field in an index */ -export interface IndexField { - /** Where the field belong */ - category: string; - /** Example of field's value */ - example?: Maybe; - /** whether the field's belong to an alias index */ - indexes: (Maybe)[]; - /** The name of the field */ - name: string; - /** The type of the field's values as recognized by Kibana */ - type: string; - /** Whether the field's values can be efficiently searched for */ - searchable: boolean; - /** Whether the field's values can be aggregated */ - aggregatable: boolean; - /** Description of the field */ - description?: Maybe; - - format?: Maybe; - /** the elastic type as mapped in the index */ - esTypes?: Maybe; - - subType?: Maybe; + indexFields: string[]; } export interface AuthenticationsData { @@ -1946,6 +1922,8 @@ export interface TimelineResult { kqlQuery?: Maybe; + indexNames?: Maybe; + notes?: Maybe; noteIds?: Maybe; @@ -2218,6 +2196,32 @@ export interface HostFields { type?: Maybe; } +/** A descriptor of a field in an index */ +export interface IndexField { + /** Where the field belong */ + category: string; + /** Example of field's value */ + example?: Maybe; + /** whether the field's belong to an alias index */ + indexes: (Maybe)[]; + /** The name of the field */ + name: string; + /** The type of the field's values as recognized by Kibana */ + type: string; + /** Whether the field's values can be efficiently searched for */ + searchable: boolean; + /** Whether the field's values can be aggregated */ + aggregatable: boolean; + /** Description of the field */ + description?: Maybe; + + format?: Maybe; + /** the elastic type as mapped in the index */ + esTypes?: Maybe; + + subType?: Maybe; +} + // ==================================================== // Arguments // ==================================================== @@ -2637,61 +2641,6 @@ export namespace GetMatrixHistogramQuery { }; } -export namespace SourceQuery { - export type Variables = { - sourceId?: Maybe; - defaultIndex: string[]; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - status: Status; - }; - - export type Status = { - __typename?: 'SourceStatus'; - - indicesExist: boolean; - - indexFields: IndexFields[]; - }; - - export type IndexFields = { - __typename?: 'IndexField'; - - category: string; - - description: Maybe; - - example: Maybe; - - indexes: (Maybe)[]; - - name: string; - - searchable: boolean; - - type: string; - - aggregatable: boolean; - - format: Maybe; - - esTypes: Maybe; - - subType: Maybe; - }; -} - export namespace GetAuthenticationsQuery { export type Variables = { sourceId: string; @@ -5269,6 +5218,8 @@ export namespace GetOneTimeline { kqlQuery: Maybe; + indexNames: Maybe; + notes: Maybe; noteIds: Maybe; @@ -5601,6 +5552,8 @@ export namespace PersistTimelineMutation { kqlQuery: Maybe; + indexNames: Maybe; + title: Maybe; dateRange: Maybe; diff --git a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx index 606b43c6508fb..4f64cca45d162 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx @@ -40,7 +40,12 @@ describe('FirstLastSeen Component', () => { useFirstLastSeenHostMock.mockReturnValue([true, MOCKED_RESPONSE]); const { container } = render( - + ); expect(container.innerHTML).toBe( @@ -52,7 +57,12 @@ describe('FirstLastSeen Component', () => { useFirstLastSeenHostMock.mockReturnValue([false, MOCKED_RESPONSE]); const { container } = render( - + ); @@ -69,7 +79,12 @@ describe('FirstLastSeen Component', () => { useFirstLastSeenHostMock.mockReturnValue([false, MOCKED_RESPONSE]); const { container } = render( - + ); await act(() => @@ -91,7 +106,12 @@ describe('FirstLastSeen Component', () => { ]); const { container } = render( - + ); @@ -114,7 +134,12 @@ describe('FirstLastSeen Component', () => { ]); const { container } = render( - + ); @@ -137,7 +162,12 @@ describe('FirstLastSeen Component', () => { ]); const { container } = render( - + ); await act(() => @@ -157,7 +187,12 @@ describe('FirstLastSeen Component', () => { ]); const { container } = render( - + ); await act(() => diff --git a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx index a1b72fb39069c..ee415560cf9de 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.tsx @@ -10,6 +10,7 @@ import React, { useMemo } from 'react'; import { useFirstLastSeenHost } from '../../containers/hosts/first_last_seen'; import { getEmptyTagValue } from '../../../common/components/empty_value'; import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; +import { DocValueFields } from '../../../../common/search_strategy'; export enum FirstLastSeenHostType { FIRST_SEEN = 'first-seen', @@ -17,47 +18,53 @@ export enum FirstLastSeenHostType { } interface FirstLastSeenHostProps { + docValueFields: DocValueFields[]; hostName: string; + indexNames: string[]; type: FirstLastSeenHostType; } -export const FirstLastSeenHost = React.memo(({ hostName, type }) => { - const [loading, { firstSeen, lastSeen, errorMessage }] = useFirstLastSeenHost({ - hostName, - }); - const valueSeen = useMemo( - () => (type === FirstLastSeenHostType.FIRST_SEEN ? firstSeen : lastSeen), - [firstSeen, lastSeen, type] - ); +export const FirstLastSeenHost = React.memo( + ({ docValueFields, hostName, type, indexNames }) => { + const [loading, { firstSeen, lastSeen, errorMessage }] = useFirstLastSeenHost({ + docValueFields, + hostName, + indexNames, + }); + const valueSeen = useMemo( + () => (type === FirstLastSeenHostType.FIRST_SEEN ? firstSeen : lastSeen), + [firstSeen, lastSeen, type] + ); + + if (errorMessage != null) { + return ( + + + + ); + } - if (errorMessage != null) { return ( - - - + <> + {loading && } + {!loading && valueSeen != null && new Date(valueSeen).toString() === 'Invalid Date' + ? valueSeen + : !loading && + valueSeen != null && ( + + + + )} + {!loading && valueSeen == null && getEmptyTagValue()} + ); } - - return ( - <> - {loading && } - {!loading && valueSeen != null && new Date(valueSeen).toString() === 'Invalid Date' - ? valueSeen - : !loading && - valueSeen != null && ( - - - - )} - {!loading && valueSeen == null && getEmptyTagValue()} - - ); -}); +); FirstLastSeenHost.displayName = 'FirstLastSeenHost'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap index 3143e680913b2..1d70f4f72ac8b 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap @@ -68,97 +68,6 @@ exports[`Hosts Table rendering it renders the default Hosts table 1`] = ` } fakeTotalCount={50} id="hostsQuery" - indexPattern={ - Object { - "fields": Array [ - Object { - "aggregatable": true, - "name": "@timestamp", - "searchable": true, - "type": "date", - }, - Object { - "aggregatable": true, - "name": "@version", - "searchable": true, - "type": "string", - }, - Object { - "aggregatable": true, - "name": "agent.ephemeral_id", - "searchable": true, - "type": "string", - }, - Object { - "aggregatable": true, - "name": "agent.hostname", - "searchable": true, - "type": "string", - }, - Object { - "aggregatable": true, - "name": "agent.id", - "searchable": true, - "type": "string", - }, - Object { - "aggregatable": true, - "name": "agent.test1", - "searchable": true, - "type": "string", - }, - Object { - "aggregatable": true, - "name": "agent.test2", - "searchable": true, - "type": "string", - }, - Object { - "aggregatable": true, - "name": "agent.test3", - "searchable": true, - "type": "string", - }, - Object { - "aggregatable": true, - "name": "agent.test4", - "searchable": true, - "type": "string", - }, - Object { - "aggregatable": true, - "name": "agent.test5", - "searchable": true, - "type": "string", - }, - Object { - "aggregatable": true, - "name": "agent.test6", - "searchable": true, - "type": "string", - }, - Object { - "aggregatable": true, - "name": "agent.test7", - "searchable": true, - "type": "string", - }, - Object { - "aggregatable": true, - "name": "agent.test8", - "searchable": true, - "type": "string", - }, - Object { - "aggregatable": true, - "name": "host.name", - "searchable": true, - "type": "string", - }, - ], - "title": "filebeat-*,auditbeat-*,packetbeat-*", - } - } isInspect={false} loadPage={[MockFunction]} loading={false} diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx index c4a391687843c..29e4dc48ae3c7 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx @@ -12,7 +12,6 @@ import { MockedProvider } from 'react-apollo/test-utils'; import '../../../common/mock/match_media'; import { apolloClientObservable, - mockIndexPattern, mockGlobalState, TestProviders, SUB_PLUGINS_REDUCER, @@ -69,7 +68,6 @@ describe('Hosts Table', () => { data={mockData.Hosts.edges} id="hostsQuery" isInspect={false} - indexPattern={mockIndexPattern} fakeTotalCount={getOr(50, 'fakeTotalCount', mockData.Hosts.pageInfo)} loading={false} loadPage={loadPage} @@ -92,7 +90,6 @@ describe('Hosts Table', () => { void; @@ -77,7 +75,6 @@ const HostsTableComponent = React.memo( direction, fakeTotalCount, id, - indexPattern, isInspect, limit, loading, diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx index 0949616827470..84003e5dea5e9 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx @@ -42,6 +42,7 @@ export const fieldsMapping: Readonly = [ const HostsKpiAuthenticationsComponent: React.FC = ({ filterQuery, from, + indexNames, to, narrowDateRange, setQuery, @@ -50,6 +51,7 @@ const HostsKpiAuthenticationsComponent: React.FC = ({ const [loading, { refetch, id, inspect, ...data }] = useHostsKpiAuthentications({ filterQuery, endDate: to, + indexNames, startDate: from, skip, }); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx index b1c4d6331e450..908ff717e2711 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx @@ -31,6 +31,7 @@ export const fieldsMapping: Readonly = [ const HostsKpiHostsComponent: React.FC = ({ filterQuery, from, + indexNames, to, narrowDateRange, setQuery, @@ -39,6 +40,7 @@ const HostsKpiHostsComponent: React.FC = ({ const [loading, { refetch, id, inspect, ...data }] = useHostsKpiHosts({ filterQuery, endDate: to, + indexNames, startDate: from, skip, }); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx index fff4c64900a8b..6174e174db5a6 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx @@ -13,12 +13,13 @@ import { HostsKpiUniqueIps } from './unique_ips'; import { HostsKpiProps } from './types'; export const HostsKpiComponent = React.memo( - ({ filterQuery, from, to, setQuery, skip, narrowDateRange }) => ( + ({ filterQuery, from, indexNames, to, setQuery, skip, narrowDateRange }) => ( ( ( ( HostsKpiComponent.displayName = 'HostsKpiComponent'; export const HostsDetailsKpiComponent = React.memo( - ({ filterQuery, from, to, setQuery, skip, narrowDateRange }) => ( + ({ filterQuery, from, indexNames, to, setQuery, skip, narrowDateRange }) => ( ( = [ const HostsKpiUniqueIpsComponent: React.FC = ({ filterQuery, from, + indexNames, to, narrowDateRange, setQuery, @@ -50,6 +51,7 @@ const HostsKpiUniqueIpsComponent: React.FC = ({ const [loading, { refetch, id, inspect, ...data }] = useHostsKpiUniqueIps({ filterQuery, endDate: to, + indexNames, startDate: from, skip, }); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx index 7bf4f7a833fb8..b1563e85c93dd 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx @@ -15,7 +15,6 @@ import { isErrorResponse, } from '../../../../../../../src/plugins/data/common'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { HostsQueries } from '../../../../common/search_strategy/security_solution'; import { HostAuthenticationsRequestOptions, @@ -56,6 +55,7 @@ interface UseAuthentications { docValueFields?: DocValueFields[]; filterQuery?: ESTermQuery | string; endDate: string; + indexNames: string[]; startDate: string; type: hostsModel.HostsType; skip: boolean; @@ -65,6 +65,7 @@ export const useAuthentications = ({ docValueFields, filterQuery, endDate, + indexNames, startDate, type, skip, @@ -74,15 +75,14 @@ export const useAuthentications = ({ (state: State) => getAuthenticationsSelector(state, type), shallowEqual ); - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [authenticationsRequest, setAuthenticationsRequest] = useState< HostAuthenticationsRequestOptions >({ - defaultIndex, + defaultIndex: indexNames, docValueFields: docValueFields ?? [], factoryQueryType: HostsQueries.authentications, filterQuery: createFilter(filterQuery), @@ -186,7 +186,7 @@ export const useAuthentications = ({ setAuthenticationsRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, docValueFields: docValueFields ?? [], filterQuery: createFilter(filterQuery), pagination: generateTablePaginationOptions(activePage, limit), @@ -201,7 +201,7 @@ export const useAuthentications = ({ } return prevRequest; }); - }, [activePage, defaultIndex, docValueFields, endDate, filterQuery, limit, skip, startDate]); + }, [activePage, docValueFields, endDate, filterQuery, indexNames, limit, skip, startDate]); useEffect(() => { authenticationsSearch(authenticationsRequest); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx index f68c340a47723..5b69e20398a35 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/_index.tsx @@ -10,7 +10,6 @@ import deepEqual from 'fast-deep-equal'; import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; import { inputsModel } from '../../../../common/store'; import { useKibana } from '../../../../common/lib/kibana'; import { @@ -41,9 +40,10 @@ export interface HostDetailsArgs { } interface UseHostDetails { - id?: string; - hostName: string; endDate: string; + hostName: string; + id?: string; + indexNames: string[]; skip?: boolean; startDate: string; } @@ -51,17 +51,17 @@ interface UseHostDetails { export const useHostDetails = ({ endDate, hostName, + indexNames, + id = ID, skip = false, startDate, - id = ID, }: UseHostDetails): [boolean, HostDetailsArgs] => { - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [hostDetailsRequest, setHostDetailsRequest] = useState({ - defaultIndex, + defaultIndex: indexNames, hostName, factoryQueryType: HostsQueries.details, timerange: { @@ -142,7 +142,7 @@ export const useHostDetails = ({ setHostDetailsRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, hostName, timerange: { interval: '12h', @@ -155,7 +155,7 @@ export const useHostDetails = ({ } return prevRequest; }); - }, [defaultIndex, endDate, hostName, startDate, skip]); + }, [endDate, hostName, indexNames, startDate, skip]); useEffect(() => { hostDetailsSearch(hostDetailsRequest); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx index 12a82c7980b61..0236270d18618 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/details/index.tsx @@ -10,11 +10,9 @@ import { Query } from 'react-apollo'; import { connect } from 'react-redux'; import { compose } from 'redux'; -import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; import { inputsModel, inputsSelectors, State } from '../../../../common/store'; import { getDefaultFetchPolicy } from '../../../../common/containers/helpers'; import { QueryTemplate, QueryTemplateProps } from '../../../../common/containers/query_template'; -import { withKibana, WithKibanaProps } from '../../../../common/lib/kibana'; import { HostOverviewQuery } from './host_overview.gql_query'; import { GetHostOverviewQuery, HostItem } from '../../../../graphql/types'; @@ -42,7 +40,7 @@ export interface OwnProps extends QueryTemplateProps { endDate: string; } -type HostsOverViewProps = OwnProps & HostOverviewReduxProps & WithKibanaProps; +type HostsOverViewProps = OwnProps & HostOverviewReduxProps; class HostOverviewByNameComponentQuery extends QueryTemplate< HostsOverViewProps, @@ -52,10 +50,10 @@ class HostOverviewByNameComponentQuery extends QueryTemplate< public render() { const { id = ID, + indexNames, isInspected, children, hostName, - kibana, skip, sourceId, startDate, @@ -75,7 +73,7 @@ class HostOverviewByNameComponentQuery extends QueryTemplate< from: startDate, to: endDate, }, - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), + defaultIndex: indexNames, inspect: isInspected, }} > @@ -108,6 +106,5 @@ const makeMapStateToProps = () => { }; export const HostOverviewByNameQuery = compose>( - connect(makeMapStateToProps), - withKibana + connect(makeMapStateToProps) )(HostOverviewByNameComponentQuery); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx index a6376642dfa29..cc944a59571f1 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/first_last_seen/index.tsx @@ -7,17 +7,15 @@ import deepEqual from 'fast-deep-equal'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; - import { useKibana } from '../../../../common/lib/kibana'; import { HostsQueries, HostFirstLastSeenRequestOptions, HostFirstLastSeenStrategyResponse, } from '../../../../../common/search_strategy/security_solution'; -import { useWithSource } from '../../../../common/containers/source'; import * as i18n from './translations'; +import { DocValueFields } from '../../../../../common/search_strategy'; import { AbortError, isCompleteResponse, @@ -33,21 +31,23 @@ export interface FirstLastSeenHostArgs { lastSeen?: string | null; } interface UseHostFirstLastSeen { + docValueFields: DocValueFields[]; hostName: string; + indexNames: string[]; } export const useFirstLastSeenHost = ({ + docValueFields, hostName, + indexNames, }: UseHostFirstLastSeen): [boolean, FirstLastSeenHostArgs] => { - const { docValueFields } = useWithSource('default'); - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [firstLastSeenHostRequest, setFirstLastSeenHostRequest] = useState< HostFirstLastSeenRequestOptions >({ - defaultIndex, + defaultIndex: indexNames, docValueFields: docValueFields ?? [], factoryQueryType: HostsQueries.firstLastSeen, hostName, @@ -124,7 +124,7 @@ export const useFirstLastSeenHost = ({ setFirstLastSeenHostRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, docValueFields: docValueFields ?? [], hostName, }; @@ -133,7 +133,7 @@ export const useFirstLastSeenHost = ({ } return prevRequest; }); - }, [defaultIndex, docValueFields, hostName]); + }, [indexNames, docValueFields, hostName]); useEffect(() => { firstLastSeenHostSearch(firstLastSeenHostRequest); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx index 2eb926a9733c3..6ca0272e58d7d 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx @@ -9,7 +9,6 @@ import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { inputsModel, State } from '../../../common/store'; import { createFilter } from '../../../common/containers/helpers'; import { useKibana } from '../../../common/lib/kibana'; @@ -54,6 +53,7 @@ interface UseAllHost { docValueFields?: DocValueFields[]; filterQuery?: ESTermQuery | string; endDate: string; + indexNames: string[]; skip?: boolean; startDate: string; type: hostsModel.HostsType; @@ -63,6 +63,7 @@ export const useAllHost = ({ docValueFields, filterQuery, endDate, + indexNames, skip = false, startDate, type, @@ -71,13 +72,12 @@ export const useAllHost = ({ const { activePage, direction, limit, sortField } = useSelector((state: State) => getHostsSelector(state, type) ); - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [hostsRequest, setHostRequest] = useState({ - defaultIndex, + defaultIndex: indexNames, docValueFields: docValueFields ?? [], factoryQueryType: HostsQueries.hosts, filterQuery: createFilter(filterQuery), @@ -181,7 +181,7 @@ export const useAllHost = ({ setHostRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, docValueFields: docValueFields ?? [], filterQuery: createFilter(filterQuery), pagination: generateTablePaginationOptions(activePage, limit), @@ -202,11 +202,11 @@ export const useAllHost = ({ }); }, [ activePage, - defaultIndex, direction, docValueFields, endDate, filterQuery, + indexNames, limit, skip, startDate, diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_host_details/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_host_details/index.tsx index 1551e7d706714..26e4eaf9ea82e 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_host_details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_host_details/index.tsx @@ -9,10 +9,8 @@ import React from 'react'; import { Query } from 'react-apollo'; import { connect, ConnectedProps } from 'react-redux'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { KpiHostDetailsData, GetKpiHostDetailsQuery } from '../../../graphql/types'; import { inputsModel, inputsSelectors, State } from '../../../common/store'; -import { useUiSetting } from '../../../common/lib/kibana'; import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; import { QueryTemplateProps } from '../../../common/containers/query_template'; @@ -33,7 +31,17 @@ export interface QueryKpiHostDetailsProps extends QueryTemplateProps { } const KpiHostDetailsComponentQuery = React.memo( - ({ id = ID, children, endDate, filterQuery, isInspected, skip, sourceId, startDate }) => ( + ({ + id = ID, + children, + endDate, + filterQuery, + indexNames, + isInspected, + skip, + sourceId, + startDate, + }) => ( query={kpiHostDetailsQuery} fetchPolicy={getDefaultFetchPolicy()} @@ -47,7 +55,7 @@ const KpiHostDetailsComponentQuery = React.memo(DEFAULT_INDEX_KEY), + defaultIndex: indexNames ?? [], inspect: isInspected, }} > diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx index 0d90b73e0a584..404231be1e6cd 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx @@ -8,7 +8,6 @@ import deepEqual from 'fast-deep-equal'; import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; import { inputsModel } from '../../../../common/store'; import { createFilter } from '../../../../common/containers/helpers'; import { useKibana } from '../../../../common/lib/kibana'; @@ -37,6 +36,7 @@ export interface HostsKpiAuthenticationsArgs interface UseHostsKpiAuthentications { filterQuery?: ESTermQuery | string; endDate: string; + indexNames: string[]; skip?: boolean; startDate: string; } @@ -44,18 +44,18 @@ interface UseHostsKpiAuthentications { export const useHostsKpiAuthentications = ({ filterQuery, endDate, + indexNames, skip = false, startDate, }: UseHostsKpiAuthentications): [boolean, HostsKpiAuthenticationsArgs] => { - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [hostsKpiAuthenticationsRequest, setHostsKpiAuthenticationsRequest] = useState< HostsKpiAuthenticationsRequestOptions >({ - defaultIndex, + defaultIndex: indexNames, factoryQueryType: HostsKpiQueries.kpiAuthentications, filterQuery: createFilter(filterQuery), id: ID, @@ -147,7 +147,7 @@ export const useHostsKpiAuthentications = ({ setHostsKpiAuthenticationsRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, filterQuery: createFilter(filterQuery), timerange: { interval: '12h', @@ -160,7 +160,7 @@ export const useHostsKpiAuthentications = ({ } return prevRequest; }); - }, [defaultIndex, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, skip, startDate]); useEffect(() => { hostsKpiAuthenticationsSearch(hostsKpiAuthenticationsRequest); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx index 190ce1aa7eae1..bb918a9214f40 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx @@ -8,7 +8,6 @@ import deepEqual from 'fast-deep-equal'; import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; import { inputsModel } from '../../../../common/store'; import { createFilter } from '../../../../common/containers/helpers'; import { useKibana } from '../../../../common/lib/kibana'; @@ -36,6 +35,7 @@ export interface HostsKpiHostsArgs extends Omit { - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [hostsKpiHostsRequest, setHostsKpiHostsRequest] = useState({ - defaultIndex, + defaultIndex: indexNames, factoryQueryType: HostsKpiQueries.kpiHosts, filterQuery: createFilter(filterQuery), id: ID, @@ -135,7 +135,7 @@ export const useHostsKpiHosts = ({ setHostsKpiHostsRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, filterQuery: createFilter(filterQuery), timerange: { interval: '12h', @@ -148,7 +148,7 @@ export const useHostsKpiHosts = ({ } return prevRequest; }); - }, [defaultIndex, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, skip, startDate]); useEffect(() => { hostsKpiHostsSearch(hostsKpiHostsRequest); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx index ac5cc12807f00..b8e93eef8dc91 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx @@ -8,7 +8,6 @@ import deepEqual from 'fast-deep-equal'; import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; import { inputsModel } from '../../../../common/store'; import { createFilter } from '../../../../common/containers/helpers'; import { useKibana } from '../../../../common/lib/kibana'; @@ -37,6 +36,7 @@ export interface HostsKpiUniqueIpsArgs interface UseHostsKpiUniqueIps { filterQuery?: ESTermQuery | string; endDate: string; + indexNames: string[]; skip?: boolean; startDate: string; } @@ -44,18 +44,18 @@ interface UseHostsKpiUniqueIps { export const useHostsKpiUniqueIps = ({ filterQuery, endDate, + indexNames, skip = false, startDate, }: UseHostsKpiUniqueIps): [boolean, HostsKpiUniqueIpsArgs] => { - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [hostsKpiUniqueIpsRequest, setHostsKpiUniqueIpsRequest] = useState< HostsKpiUniqueIpsRequestOptions >({ - defaultIndex, + defaultIndex: indexNames, factoryQueryType: HostsKpiQueries.kpiUniqueIps, filterQuery: createFilter(filterQuery), id: ID, @@ -144,7 +144,7 @@ export const useHostsKpiUniqueIps = ({ setHostsKpiUniqueIpsRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, filterQuery: createFilter(filterQuery), timerange: { interval: '12h', @@ -157,7 +157,7 @@ export const useHostsKpiUniqueIps = ({ } return prevRequest; }); - }, [defaultIndex, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, skip, startDate]); useEffect(() => { hostsKpiUniqueIpsSearch(hostsKpiUniqueIpsRequest); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx index e28a808378dd7..4036837024025 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx @@ -15,7 +15,6 @@ import { isErrorResponse, } from '../../../../../../../src/plugins/data/common'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { inputsModel, State } from '../../../common/store'; import { useKibana } from '../../../common/lib/kibana'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; @@ -53,6 +52,7 @@ interface UseUncommonProcesses { docValueFields?: DocValueFields[]; filterQuery?: ESTermQuery | string; endDate: string; + indexNames: string[]; skip?: boolean; startDate: string; type: hostsModel.HostsType; @@ -62,6 +62,7 @@ export const useUncommonProcesses = ({ docValueFields, filterQuery, endDate, + indexNames, skip = false, startDate, type, @@ -70,15 +71,14 @@ export const useUncommonProcesses = ({ const { activePage, limit } = useSelector((state: State) => getUncommonProcessesSelector(state, type) ); - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [uncommonProcessesRequest, setUncommonProcessesRequest] = useState< HostsUncommonProcessesRequestOptions >({ - defaultIndex, + defaultIndex: indexNames, docValueFields: docValueFields ?? [], factoryQueryType: HostsQueries.uncommonProcesses, filterQuery: createFilter(filterQuery), @@ -186,7 +186,7 @@ export const useUncommonProcesses = ({ setUncommonProcessesRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, docValueFields: docValueFields ?? [], filterQuery: createFilter(filterQuery), pagination: generateTablePaginationOptions(activePage, limit), @@ -202,7 +202,7 @@ export const useUncommonProcesses = ({ } return prevRequest; }); - }, [activePage, defaultIndex, docValueFields, endDate, filterQuery, limit, skip, startDate]); + }, [activePage, indexNames, docValueFields, endDate, filterQuery, limit, skip, startDate]); useEffect(() => { uncommonProcessesSearch(uncommonProcessesRequest); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx index 11a268c7b64ad..708c8b2b40b35 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx @@ -84,6 +84,7 @@ describe('body', () => { setQuery={jest.fn()} setAbsoluteRangeDatePicker={(jest.fn() as unknown) as SetAbsoluteRangeDatePicker} hostDetailsPagePath={hostDetailsPagePath} + indexNames={[]} indexPattern={mockIndexPattern} type={type} pageFilters={mockHostDetailsPageFilters} diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx index 4d4eead0e778a..284e6e27cf615 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx @@ -28,12 +28,13 @@ import { export const HostDetailsTabs = React.memo( ({ + detailName, docValueFields, - pageFilters, filterQuery, - detailName, - setAbsoluteRangeDatePicker, + indexNames, indexPattern, + pageFilters, + setAbsoluteRangeDatePicker, hostDetailsPagePath, }) => { const { from, to, isInitializing, deleteQuery, setQuery } = useGlobalTime(); @@ -73,6 +74,7 @@ export const HostDetailsTabs = React.memo( startDate: from, type, indexPattern, + indexNames, hostName: detailName, narrowDateRange, updateDateRange, diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index 57e1b128ce64d..55b2b529000be 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -28,7 +28,6 @@ import { SiemSearchBar } from '../../../common/components/search_bar'; import { WrapperPage } from '../../../common/components/wrapper_page'; import { HostOverviewByNameQuery } from '../../containers/hosts/details'; import { useGlobalTime } from '../../../common/containers/use_global_time'; -import { useWithSource } from '../../../common/containers/source'; import { LastEventIndexKey } from '../../../graphql/types'; import { useKibana } from '../../../common/lib/kibana'; import { convertToBuildEsQuery } from '../../../common/lib/keury'; @@ -51,6 +50,7 @@ import { timelineSelectors } from '../../../timelines/store/timeline'; import { TimelineModel } from '../../../timelines/store/timeline/model'; import { TimelineId } from '../../../../common/types/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; +import { useSourcererScope } from '../../../common/containers/sourcerer'; const HostOverviewManage = manageQuery(HostOverview); @@ -89,7 +89,7 @@ const HostDetailsComponent = React.memo( }, [setAbsoluteRangeDatePicker] ); - const { docValueFields, indicesExist, indexPattern } = useWithSource(); + const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope(); const filterQuery = convertToBuildEsQuery({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), indexPattern, @@ -111,12 +111,18 @@ const HostDetailsComponent = React.memo( + } title={detailName} /> ( > {({ isLoadingAnomaliesData, anomaliesData }) => ( ( data={hostOverview as HostItem} anomaliesData={anomaliesData} isLoadingAnomaliesData={isLoadingAnomaliesData} + indexNames={selectedPatterns} loading={loading} startDate={from} endDate={to} @@ -161,6 +169,7 @@ const HostDetailsComponent = React.memo( ( ; export type HostDetailsTabsProps = HostBodyComponentDispatchProps & HostsQueryProps & { docValueFields?: DocValueFields[]; + indexNames: string[]; pageFilters?: Filter[]; filterQuery: string; indexPattern: IIndexPattern; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx index 566f8f23efd39..b341647afdfbc 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx @@ -10,7 +10,6 @@ import { Router } from 'react-router-dom'; import { Filter } from '../../../../../../src/plugins/data/common/es_query'; import '../../common/mock/match_media'; -import { useWithSource } from '../../common/containers/source'; import { apolloClientObservable, TestProviders, @@ -25,8 +24,9 @@ import { State, createStore } from '../../common/store'; import { HostsComponentProps } from './types'; import { Hosts } from './hosts'; import { HostsTabs } from './hosts_tabs'; +import { useSourcererScope } from '../../common/containers/sourcerer'; -jest.mock('../../common/containers/source'); +jest.mock('../../common/containers/sourcerer'); // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar @@ -58,14 +58,14 @@ const mockHistory = { createHref: jest.fn(), listen: jest.fn(), }; - +const mockUseSourcererScope = useSourcererScope as jest.Mock; describe('Hosts - rendering', () => { const hostProps: HostsComponentProps = { hostsPagePath: '', }; test('it renders the Setup Instructions text when no index is available', async () => { - (useWithSource as jest.Mock).mockReturnValue({ + mockUseSourcererScope.mockReturnValue({ indicesExist: false, }); @@ -80,7 +80,7 @@ describe('Hosts - rendering', () => { }); test('it DOES NOT render the Setup Instructions text when an index is available', async () => { - (useWithSource as jest.Mock).mockReturnValue({ + mockUseSourcererScope.mockReturnValue({ indicesExist: true, indexPattern: {}, }); @@ -95,7 +95,7 @@ describe('Hosts - rendering', () => { }); test('it should render tab navigation', async () => { - (useWithSource as jest.Mock).mockReturnValue({ + mockUseSourcererScope.mockReturnValue({ indicesExist: true, indexPattern: {}, }); @@ -142,7 +142,7 @@ describe('Hosts - rendering', () => { }, }, ]; - (useWithSource as jest.Mock).mockReturnValue({ + mockUseSourcererScope.mockReturnValue({ indicesExist: true, indexPattern: { fields: [], title: 'title' }, }); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index 4b8e3cc6987ac..ea8cf11e7595a 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -22,7 +22,6 @@ import { SiemSearchBar } from '../../common/components/search_bar'; import { WrapperPage } from '../../common/components/wrapper_page'; import { useFullScreen } from '../../common/containers/use_full_screen'; import { useGlobalTime } from '../../common/containers/use_global_time'; -import { useWithSource } from '../../common/containers/source'; import { TimelineId } from '../../../common/types/timeline'; import { LastEventIndexKey } from '../../graphql/types'; import { useKibana } from '../../common/lib/kibana'; @@ -46,6 +45,7 @@ import { showGlobalFilters } from '../../timelines/components/timeline/helpers'; import { timelineSelectors } from '../../timelines/store/timeline'; import { timelineDefaults } from '../../timelines/store/timeline/defaults'; import { TimelineModel } from '../../timelines/store/timeline/model'; +import { useSourcererScope } from '../../common/containers/sourcerer'; export const HostsComponent = React.memo( ({ filters, graphEventId, query, setAbsoluteRangeDatePicker, hostsPagePath }) => { @@ -74,7 +74,7 @@ export const HostsComponent = React.memo( }, [setAbsoluteRangeDatePicker] ); - const { docValueFields, indicesExist, indexPattern } = useWithSource(); + const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope(); const filterQuery = convertToBuildEsQuery({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), indexPattern, @@ -101,12 +101,19 @@ export const HostsComponent = React.memo( } + subtitle={ + + } title={i18n.PAGE_TITLE} /> ( to={to} filterQuery={tabsFilterQuery} isInitializing={isInitializing} + indexNames={selectedPatterns} setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} setQuery={setQuery} from={from} type={hostsModel.HostsType.page} - indexPattern={indexPattern} hostsPagePath={hostsPagePath} /> diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx index 8e2ea06fd20cb..17dd20bac2d0d 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx @@ -28,24 +28,24 @@ export const HostsTabs = memo( deleteQuery, docValueFields, filterQuery, - setAbsoluteRangeDatePicker, - to, from, - setQuery, + indexNames, isInitializing, - type, - indexPattern, hostsPagePath, + setAbsoluteRangeDatePicker, + setQuery, + to, + type, }) => { const tabProps = { deleteQuery, endDate: to, filterQuery, + indexNames, skip: isInitializing, setQuery, startDate: from, type, - indexPattern, narrowDateRange: useCallback( (score: Anomaly, interval: string) => { const fromTo = scoreIntervalToDateTime(score, interval); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx index d3fc68874ce91..efce312fd85f2 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx @@ -66,6 +66,7 @@ const AuthenticationsQueryTabBodyComponent: React.FC docValueFields, endDate, filterQuery, + indexNames, skip, setQuery, startDate, @@ -74,7 +75,15 @@ const AuthenticationsQueryTabBodyComponent: React.FC const [ loading, { authentications, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, - ] = useAuthentications({ docValueFields, endDate, filterQuery, skip, startDate, type }); + ] = useAuthentications({ + docValueFields, + endDate, + filterQuery, + indexNames, + skip, + startDate, + type, + }); useEffect(() => { return () => { @@ -90,6 +99,7 @@ const AuthenticationsQueryTabBodyComponent: React.FC endDate={endDate} filterQuery={filterQuery} id={ID} + indexNames={indexNames} setQuery={setQuery} startDate={startDate} {...histogramConfigs} diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx index be8412caf7732..e30071ec04f0c 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx @@ -20,6 +20,7 @@ import { useFullScreen } from '../../../common/containers/use_full_screen'; import * as i18n from '../translations'; import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; const EVENTS_HISTOGRAM_ID = 'eventsHistogramQuery'; @@ -54,6 +55,7 @@ const EventsQueryTabBodyComponent: React.FC = ({ deleteQuery, endDate, filterQuery, + indexNames, pageFilters, setQuery, startDate, @@ -85,6 +87,7 @@ const EventsQueryTabBodyComponent: React.FC = ({ setQuery={setQuery} startDate={startDate} id={EVENTS_HISTOGRAM_ID} + indexNames={indexNames} {...histogramConfigs} /> )} @@ -92,6 +95,7 @@ const EventsQueryTabBodyComponent: React.FC = ({ defaultModel={eventsDefaultModel} end={endDate} id={TimelineId.hostsPageEvents} + scopeId={SourcererScopeName.default} start={startDate} pageFilters={pageFilters} /> diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx index f8dcf9635c053..deda4b618fa64 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx @@ -18,7 +18,7 @@ export const HostsQueryTabBody = ({ docValueFields, endDate, filterQuery, - indexPattern, + indexNames, skip, setQuery, startDate, @@ -27,7 +27,7 @@ export const HostsQueryTabBody = ({ const [ loading, { hosts, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, - ] = useAllHost({ docValueFields, endDate, filterQuery, skip, startDate, type }); + ] = useAllHost({ docValueFields, endDate, filterQuery, indexNames, skip, startDate, type }); return ( - + {actions} diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index 235f150637116..40c982cfc071b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -203,6 +203,7 @@ export const PolicyDetails = React.memo(() => { )}
+ +
+
+ +
+
+
+ +
+
+ +
+
+
{ reactTestingLibrary.act(() => { history.push('/trusted_apps'); }); + window.scrollTo = jest.fn(); }); test.skip('rendering', () => { diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap index a03d7c2317517..c512bd99a7916 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap @@ -4,6 +4,7 @@ exports[`NetworkKpiComponent rendering it renders the default widget 1`] = ` = [ const NetworkKpiDnsComponent: React.FC = ({ filterQuery, from, + indexNames, to, narrowDateRange, setQuery, @@ -36,6 +37,7 @@ const NetworkKpiDnsComponent: React.FC = ({ const [loading, { refetch, id, inspect, ...data }] = useNetworkKpiDns({ filterQuery, endDate: to, + indexNames, startDate: from, skip, }); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx index 9c6b2fe3c8ca4..25a9fe03f9205 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.test.tsx @@ -22,12 +22,13 @@ import { NetworkKpiComponent } from '.'; describe('NetworkKpiComponent', () => { const state: State = mockGlobalState; const props = { + filterQuery: '', from: '2019-06-15T06:00:00.000Z', - to: '2019-06-18T06:00:00.000Z', + indexNames: [], narrowDateRange: jest.fn(), - filterQuery: '', setQuery: jest.fn(), skip: true, + to: '2019-06-18T06:00:00.000Z', }; const { storage } = createSecuritySolutionStorageMock(); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx index 95534e1a61988..1a04d1cc2c0eb 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx @@ -15,7 +15,7 @@ import { NetworkKpiUniquePrivateIps } from './unique_private_ips'; import { NetworkKpiProps } from './types'; export const NetworkKpiComponent = React.memo( - ({ filterQuery, from, to, setQuery, skip, narrowDateRange }) => ( + ({ filterQuery, from, indexNames, to, setQuery, skip, narrowDateRange }) => ( @@ -23,6 +23,7 @@ export const NetworkKpiComponent = React.memo( ( ( ( ( = [ const NetworkKpiNetworkEventsComponent: React.FC = ({ filterQuery, from, + indexNames, to, narrowDateRange, setQuery, @@ -41,6 +42,7 @@ const NetworkKpiNetworkEventsComponent: React.FC = ({ const [loading, { refetch, id, inspect, ...data }] = useNetworkKpiNetworkEvents({ filterQuery, endDate: to, + indexNames, startDate: from, skip, }); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.tsx index 575d4256e8395..500314446bc12 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.tsx @@ -28,6 +28,7 @@ export const fieldsMapping: Readonly = [ const NetworkKpiTlsHandshakesComponent: React.FC = ({ filterQuery, from, + indexNames, to, narrowDateRange, setQuery, @@ -36,6 +37,7 @@ const NetworkKpiTlsHandshakesComponent: React.FC = ({ const [loading, { refetch, id, inspect, ...data }] = useNetworkKpiTlsHandshakes({ filterQuery, endDate: to, + indexNames, startDate: from, skip, }); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/types.ts b/x-pack/plugins/security_solution/public/network/components/kpi_network/types.ts index d3a0ac5a6c5dd..860e6e228bbaa 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/types.ts +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/types.ts @@ -10,6 +10,7 @@ import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; export interface NetworkKpiProps { filterQuery: string; from: string; + indexNames: string[]; to: string; narrowDateRange: UpdateDateRange; setQuery: GlobalTimeArgs['setQuery']; diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.tsx index d22d4454952f9..65624ba481378 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.tsx @@ -28,6 +28,7 @@ export const fieldsMapping: Readonly = [ const NetworkKpiUniqueFlowsComponent: React.FC = ({ filterQuery, from, + indexNames, to, narrowDateRange, setQuery, @@ -36,6 +37,7 @@ const NetworkKpiUniqueFlowsComponent: React.FC = ({ const [loading, { refetch, id, inspect, ...data }] = useNetworkKpiUniqueFlows({ filterQuery, endDate: to, + indexNames, startDate: from, skip, }); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.tsx index a7dfb38219c07..a8a179b97f51a 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.tsx @@ -47,6 +47,7 @@ export const fieldsMapping: Readonly = [ const NetworkKpiUniquePrivateIpsComponent: React.FC = ({ filterQuery, from, + indexNames, to, narrowDateRange, setQuery, @@ -55,6 +56,7 @@ const NetworkKpiUniquePrivateIpsComponent: React.FC = ({ const [loading, { refetch, id, inspect, ...data }] = useNetworkKpiUniquePrivateIps({ filterQuery, endDate: to, + indexNames, startDate: from, skip, }); diff --git a/x-pack/plugins/security_solution/public/network/containers/details/index.tsx b/x-pack/plugins/security_solution/public/network/containers/details/index.tsx index f6ea86bd552f4..217241bdadcbb 100644 --- a/x-pack/plugins/security_solution/public/network/containers/details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/details/index.tsx @@ -9,7 +9,6 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import deepEqual from 'fast-deep-equal'; import { ESTermQuery } from '../../../../common/typed_json'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { inputsModel } from '../../../common/store'; import { useKibana } from '../../../common/lib/kibana'; import { createFilter } from '../../../common/containers/helpers'; @@ -42,6 +41,7 @@ interface UseNetworkDetails { id?: string; docValueFields: DocValueFields[]; ip: string; + indexNames: string[]; filterQuery?: ESTermQuery | string; skip: boolean; } @@ -49,18 +49,18 @@ interface UseNetworkDetails { export const useNetworkDetails = ({ docValueFields, filterQuery, + indexNames, id = ID, skip, ip, }: UseNetworkDetails): [boolean, NetworkDetailsArgs] => { - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [networkDetailsRequest, setNetworkDetailsRequest] = useState({ - defaultIndex, + defaultIndex: indexNames, docValueFields: docValueFields ?? [], factoryQueryType: NetworkQueries.details, filterQuery: createFilter(filterQuery), @@ -137,7 +137,7 @@ export const useNetworkDetails = ({ setNetworkDetailsRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, ip, docValueFields: docValueFields ?? [], filterQuery: createFilter(filterQuery), @@ -147,7 +147,7 @@ export const useNetworkDetails = ({ } return prevRequest; }); - }, [defaultIndex, filterQuery, skip, ip, docValueFields]); + }, [indexNames, filterQuery, skip, ip, docValueFields]); useEffect(() => { networkDetailsSearch(networkDetailsRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx index 2afbff3138c6b..dc60bb0a82ba8 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx @@ -8,7 +8,6 @@ import deepEqual from 'fast-deep-equal'; import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; import { inputsModel } from '../../../../common/store'; import { createFilter } from '../../../../common/containers/helpers'; import { useKibana } from '../../../../common/lib/kibana'; @@ -41,6 +40,7 @@ export interface NetworkKpiDnsArgs { interface UseNetworkKpiDns { filterQuery?: ESTermQuery | string; endDate: string; + indexNames: string[]; skip?: boolean; startDate: string; } @@ -48,16 +48,16 @@ interface UseNetworkKpiDns { export const useNetworkKpiDns = ({ filterQuery, endDate, + indexNames, skip = false, startDate, }: UseNetworkKpiDns): [boolean, NetworkKpiDnsArgs] => { - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [networkKpiDnsRequest, setNetworkKpiDnsRequest] = useState({ - defaultIndex, + defaultIndex: indexNames, factoryQueryType: NetworkKpiQueries.dns, filterQuery: createFilter(filterQuery), id: ID, @@ -138,7 +138,7 @@ export const useNetworkKpiDns = ({ setNetworkKpiDnsRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, filterQuery: createFilter(filterQuery), timerange: { interval: '12h', @@ -151,7 +151,7 @@ export const useNetworkKpiDns = ({ } return prevRequest; }); - }, [defaultIndex, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, skip, startDate]); useEffect(() => { networkKpiDnsSearch(networkKpiDnsRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx index 26b57ef36b09d..a1727d5bb4331 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx @@ -8,7 +8,6 @@ import deepEqual from 'fast-deep-equal'; import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; import { inputsModel } from '../../../../common/store'; import { createFilter } from '../../../../common/containers/helpers'; import { useKibana } from '../../../../common/lib/kibana'; @@ -41,6 +40,7 @@ export interface NetworkKpiNetworkEventsArgs { interface UseNetworkKpiNetworkEvents { filterQuery?: ESTermQuery | string; endDate: string; + indexNames: string[]; skip?: boolean; startDate: string; } @@ -48,18 +48,18 @@ interface UseNetworkKpiNetworkEvents { export const useNetworkKpiNetworkEvents = ({ filterQuery, endDate, + indexNames, skip = false, startDate, }: UseNetworkKpiNetworkEvents): [boolean, NetworkKpiNetworkEventsArgs] => { - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [networkKpiNetworkEventsRequest, setNetworkKpiNetworkEventsRequest] = useState< NetworkKpiNetworkEventsRequestOptions >({ - defaultIndex, + defaultIndex: indexNames, factoryQueryType: NetworkKpiQueries.networkEvents, filterQuery: createFilter(filterQuery), id: ID, @@ -145,7 +145,7 @@ export const useNetworkKpiNetworkEvents = ({ setNetworkKpiNetworkEventsRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, filterQuery: createFilter(filterQuery), timerange: { interval: '12h', @@ -158,7 +158,7 @@ export const useNetworkKpiNetworkEvents = ({ } return prevRequest; }); - }, [defaultIndex, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, skip, startDate]); useEffect(() => { networkKpiNetworkEventsSearch(networkKpiNetworkEventsRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx index c97c1e43e699a..bcbe485e82163 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx @@ -8,7 +8,6 @@ import deepEqual from 'fast-deep-equal'; import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; import { inputsModel } from '../../../../common/store'; import { createFilter } from '../../../../common/containers/helpers'; import { useKibana } from '../../../../common/lib/kibana'; @@ -41,6 +40,7 @@ export interface NetworkKpiTlsHandshakesArgs { interface UseNetworkKpiTlsHandshakes { filterQuery?: ESTermQuery | string; endDate: string; + indexNames: string[]; skip?: boolean; startDate: string; } @@ -48,18 +48,18 @@ interface UseNetworkKpiTlsHandshakes { export const useNetworkKpiTlsHandshakes = ({ filterQuery, endDate, + indexNames, skip = false, startDate, }: UseNetworkKpiTlsHandshakes): [boolean, NetworkKpiTlsHandshakesArgs] => { - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [networkKpiTlsHandshakesRequest, setNetworkKpiTlsHandshakesRequest] = useState< NetworkKpiTlsHandshakesRequestOptions >({ - defaultIndex, + defaultIndex: indexNames, factoryQueryType: NetworkKpiQueries.tlsHandshakes, filterQuery: createFilter(filterQuery), id: ID, @@ -145,7 +145,7 @@ export const useNetworkKpiTlsHandshakes = ({ setNetworkKpiTlsHandshakesRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, filterQuery: createFilter(filterQuery), timerange: { interval: '12h', @@ -158,7 +158,7 @@ export const useNetworkKpiTlsHandshakes = ({ } return prevRequest; }); - }, [defaultIndex, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, skip, startDate]); useEffect(() => { networkKpiTlsHandshakesSearch(networkKpiTlsHandshakesRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx index 4e8b4ad38b711..a4fdefc93fe75 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx @@ -8,7 +8,6 @@ import deepEqual from 'fast-deep-equal'; import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; import { inputsModel } from '../../../../common/store'; import { createFilter } from '../../../../common/containers/helpers'; import { useKibana } from '../../../../common/lib/kibana'; @@ -41,6 +40,7 @@ export interface NetworkKpiUniqueFlowsArgs { interface UseNetworkKpiUniqueFlows { filterQuery?: ESTermQuery | string; endDate: string; + indexNames: string[]; skip?: boolean; startDate: string; } @@ -48,18 +48,18 @@ interface UseNetworkKpiUniqueFlows { export const useNetworkKpiUniqueFlows = ({ filterQuery, endDate, + indexNames, skip = false, startDate, }: UseNetworkKpiUniqueFlows): [boolean, NetworkKpiUniqueFlowsArgs] => { - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [networkKpiUniqueFlowsRequest, setNetworkKpiUniqueFlowsRequest] = useState< NetworkKpiUniqueFlowsRequestOptions >({ - defaultIndex, + defaultIndex: indexNames, factoryQueryType: NetworkKpiQueries.uniqueFlows, filterQuery: createFilter(filterQuery), id: ID, @@ -145,7 +145,7 @@ export const useNetworkKpiUniqueFlows = ({ setNetworkKpiUniqueFlowsRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, filterQuery: createFilter(filterQuery), timerange: { interval: '12h', @@ -158,7 +158,7 @@ export const useNetworkKpiUniqueFlows = ({ } return prevRequest; }); - }, [defaultIndex, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, skip, startDate]); useEffect(() => { networkKpiUniqueFlowsSearch(networkKpiUniqueFlowsRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx index b518f95212129..5e9d829077f23 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx @@ -8,7 +8,6 @@ import deepEqual from 'fast-deep-equal'; import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; import { inputsModel } from '../../../../common/store'; import { createFilter } from '../../../../common/containers/helpers'; import { useKibana } from '../../../../common/lib/kibana'; @@ -45,6 +44,7 @@ export interface NetworkKpiUniquePrivateIpsArgs { interface UseNetworkKpiUniquePrivateIps { filterQuery?: ESTermQuery | string; endDate: string; + indexNames: string[]; skip?: boolean; startDate: string; } @@ -52,18 +52,18 @@ interface UseNetworkKpiUniquePrivateIps { export const useNetworkKpiUniquePrivateIps = ({ filterQuery, endDate, + indexNames, skip = false, startDate, }: UseNetworkKpiUniquePrivateIps): [boolean, NetworkKpiUniquePrivateIpsArgs] => { - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [networkKpiUniquePrivateIpsRequest, setNetworkKpiUniquePrivateIpsRequest] = useState< NetworkKpiUniquePrivateIpsRequestOptions >({ - defaultIndex, + defaultIndex: indexNames, factoryQueryType: NetworkKpiQueries.uniquePrivateIps, filterQuery: createFilter(filterQuery), id: ID, @@ -156,7 +156,7 @@ export const useNetworkKpiUniquePrivateIps = ({ setNetworkKpiUniquePrivateIpsRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, filterQuery: createFilter(filterQuery), timerange: { interval: '12h', @@ -169,7 +169,7 @@ export const useNetworkKpiUniquePrivateIps = ({ } return prevRequest; }); - }, [defaultIndex, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, skip, startDate]); useEffect(() => { networkKpiUniquePrivateIpsSearch(networkKpiUniquePrivateIpsRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx index 209f8da0d8fae..334373c4a551a 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx @@ -10,7 +10,6 @@ import { shallowEqual, useSelector } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { ESTermQuery } from '../../../../common/typed_json'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { inputsModel, State } from '../../../common/store'; import { useKibana } from '../../../common/lib/kibana'; import { createFilter } from '../../../common/containers/helpers'; @@ -51,6 +50,7 @@ export interface NetworkDnsArgs { interface UseNetworkDns { id?: string; + indexNames: string[]; type: networkModel.NetworkType; filterQuery?: ESTermQuery | string; endDate: string; @@ -62,6 +62,7 @@ export const useNetworkDns = ({ endDate, filterQuery, id = ID, + indexNames, skip, startDate, type, @@ -71,14 +72,13 @@ export const useNetworkDns = ({ (state: State) => getNetworkDnsSelector(state), shallowEqual ); - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [networkDnsRequest, setNetworkDnsRequest] = useState({ - defaultIndex, + defaultIndex: indexNames, factoryQueryType: NetworkQueries.dns, filterQuery: createFilter(filterQuery), isPtrIncluded, @@ -182,7 +182,7 @@ export const useNetworkDns = ({ setNetworkDnsRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, isPtrIncluded, filterQuery: createFilter(filterQuery), pagination: generateTablePaginationOptions(activePage, limit), @@ -198,7 +198,7 @@ export const useNetworkDns = ({ } return prevRequest; }); - }, [activePage, defaultIndex, endDate, filterQuery, limit, startDate, sort, skip, isPtrIncluded]); + }, [activePage, indexNames, endDate, filterQuery, limit, startDate, sort, skip, isPtrIncluded]); useEffect(() => { networkDnsSearch(networkDnsRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx index 9244d571bb67b..221b693818c50 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx @@ -10,7 +10,6 @@ import { shallowEqual, useSelector } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { ESTermQuery } from '../../../../common/typed_json'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { inputsModel, State } from '../../../common/store'; import { useKibana } from '../../../common/lib/kibana'; import { createFilter } from '../../../common/containers/helpers'; @@ -49,6 +48,7 @@ export interface NetworkHttpArgs { interface UseNetworkHttp { id?: string; ip?: string; + indexNames: string[]; type: networkModel.NetworkType; filterQuery?: ESTermQuery | string; endDate: string; @@ -60,6 +60,7 @@ export const useNetworkHttp = ({ endDate, filterQuery, id = ID, + indexNames, ip, skip, startDate, @@ -70,14 +71,13 @@ export const useNetworkHttp = ({ (state: State) => getHttpSelector(state, type), shallowEqual ); - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [networkHttpRequest, setHostRequest] = useState({ - defaultIndex, + defaultIndex: indexNames, factoryQueryType: NetworkQueries.http, filterQuery: createFilter(filterQuery), ip, @@ -181,7 +181,7 @@ export const useNetworkHttp = ({ setHostRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, filterQuery: createFilter(filterQuery), pagination: generateTablePaginationOptions(activePage, limit), sort: sort as SortField, @@ -196,7 +196,7 @@ export const useNetworkHttp = ({ } return prevRequest; }); - }, [activePage, defaultIndex, endDate, filterQuery, limit, startDate, sort, skip]); + }, [activePage, indexNames, endDate, filterQuery, limit, startDate, sort, skip]); useEffect(() => { networkHttpSearch(networkHttpRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx index 8138d30f2c510..6b52966342e97 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx @@ -10,7 +10,6 @@ import { shallowEqual, useSelector } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { ESTermQuery } from '../../../../common/typed_json'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { inputsModel, State } from '../../../common/store'; import { useKibana } from '../../../common/lib/kibana'; import { createFilter } from '../../../common/containers/helpers'; @@ -49,6 +48,7 @@ export interface NetworkTopCountriesArgs { interface UseNetworkTopCountries { flowTarget: FlowTargetSourceDest; ip?: string; + indexNames: string[]; type: networkModel.NetworkType; filterQuery?: ESTermQuery | string; endDate: string; @@ -60,6 +60,7 @@ export const useNetworkTopCountries = ({ endDate, filterQuery, flowTarget, + indexNames, skip, startDate, type, @@ -69,14 +70,13 @@ export const useNetworkTopCountries = ({ (state: State) => getTopCountriesSelector(state, type, flowTarget), shallowEqual ); - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [networkTopCountriesRequest, setHostRequest] = useState({ - defaultIndex, + defaultIndex: indexNames, factoryQueryType: NetworkQueries.topCountries, filterQuery: createFilter(filterQuery), flowTarget, @@ -180,7 +180,7 @@ export const useNetworkTopCountries = ({ setHostRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, filterQuery: createFilter(filterQuery), pagination: generateTablePaginationOptions(activePage, limit), sort, @@ -195,7 +195,7 @@ export const useNetworkTopCountries = ({ } return prevRequest; }); - }, [activePage, defaultIndex, endDate, filterQuery, limit, startDate, sort, skip]); + }, [activePage, indexNames, endDate, filterQuery, limit, startDate, sort, skip]); useEffect(() => { networkTopCountriesSearch(networkTopCountriesRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx index 76c2ae2871a38..d6dd14b3259f0 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx @@ -10,7 +10,6 @@ import { shallowEqual, useSelector } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { ESTermQuery } from '../../../../common/typed_json'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { inputsModel, State } from '../../../common/store'; import { useKibana } from '../../../common/lib/kibana'; import { createFilter } from '../../../common/containers/helpers'; @@ -49,6 +48,7 @@ export interface NetworkTopNFlowArgs { interface UseNetworkTopNFlow { flowTarget: FlowTargetSourceDest; ip?: string; + indexNames: string[]; type: networkModel.NetworkType; filterQuery?: ESTermQuery | string; endDate: string; @@ -60,6 +60,7 @@ export const useNetworkTopNFlow = ({ endDate, filterQuery, flowTarget, + indexNames, skip, startDate, type, @@ -69,14 +70,13 @@ export const useNetworkTopNFlow = ({ (state: State) => getTopNFlowSelector(state, type, flowTarget), shallowEqual ); - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [networkTopNFlowRequest, setTopNFlowRequest] = useState({ - defaultIndex, + defaultIndex: indexNames, factoryQueryType: NetworkQueries.topNFlow, filterQuery: createFilter(filterQuery), flowTarget, @@ -178,7 +178,7 @@ export const useNetworkTopNFlow = ({ setTopNFlowRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, filterQuery: createFilter(filterQuery), pagination: generateTablePaginationOptions(activePage, limit), timerange: { @@ -193,7 +193,7 @@ export const useNetworkTopNFlow = ({ } return prevRequest; }); - }, [activePage, defaultIndex, endDate, filterQuery, limit, startDate, sort, skip]); + }, [activePage, indexNames, endDate, filterQuery, limit, startDate, sort, skip]); useEffect(() => { networkTopNFlowSearch(networkTopNFlowRequest); diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx index 4c9658aa9b42c..f40675a1255ff 100644 --- a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx @@ -10,7 +10,6 @@ import { shallowEqual, useSelector } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { ESTermQuery } from '../../../../common/typed_json'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { inputsModel, State } from '../../../common/store'; import { useKibana } from '../../../common/lib/kibana'; import { createFilter } from '../../../common/containers/helpers'; @@ -46,6 +45,7 @@ export interface NetworkTlsArgs { interface UseNetworkTls { flowTarget: FlowTargetSourceDest; + indexNames: string[]; ip: string; type: networkModel.NetworkType; filterQuery?: ESTermQuery | string; @@ -60,6 +60,7 @@ export const useNetworkTls = ({ filterQuery, flowTarget, id = ID, + indexNames, ip, skip, startDate, @@ -70,14 +71,13 @@ export const useNetworkTls = ({ (state: State) => getTlsSelector(state, type, flowTarget), shallowEqual ); - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [networkTlsRequest, setHostRequest] = useState({ - defaultIndex, + defaultIndex: indexNames, factoryQueryType: NetworkQueries.tls, filterQuery: createFilter(filterQuery), flowTarget, @@ -178,7 +178,7 @@ export const useNetworkTls = ({ setHostRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, filterQuery: createFilter(filterQuery), pagination: generateTablePaginationOptions(activePage, limit), timerange: { @@ -193,7 +193,7 @@ export const useNetworkTls = ({ } return prevRequest; }); - }, [activePage, defaultIndex, endDate, filterQuery, limit, startDate, sort, skip]); + }, [activePage, indexNames, endDate, filterQuery, limit, startDate, sort, skip]); useEffect(() => { networkTlsSearch(networkTlsRequest); diff --git a/x-pack/plugins/security_solution/public/network/pages/details/index.test.tsx b/x-pack/plugins/security_solution/public/network/pages/details/index.test.tsx index beae3e4f72aaa..430b5702be1bc 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/index.test.tsx @@ -9,7 +9,7 @@ import { Router, useParams } from 'react-router-dom'; import '../../../common/mock/match_media'; -import { useWithSource } from '../../../common/containers/source'; +import { useSourcererScope } from '../../../common/containers/sourcerer'; import { FlowTarget } from '../../../graphql/types'; import { apolloClientObservable, @@ -40,7 +40,7 @@ jest.mock('../../containers/details', () => ({ useNetworkDetails: jest.fn().mockReturnValue([true, { networkDetails: {} }]), })); jest.mock('../../../common/lib/kibana'); -jest.mock('../../../common/containers/source'); +jest.mock('../../../common/containers/sourcerer'); jest.mock('../../../common/containers/use_global_time', () => ({ useGlobalTime: jest.fn().mockReturnValue({ from: '2020-07-07T08:20:18.966Z', @@ -81,7 +81,7 @@ const getMockHistory = (ip: string) => ({ describe('Network Details', () => { const mount = useMountAppended(); beforeAll(() => { - (useWithSource as jest.Mock).mockReturnValue({ + (useSourcererScope as jest.Mock).mockReturnValue({ indicesExist: false, indexPattern: {}, }); @@ -137,7 +137,7 @@ describe('Network Details', () => { test('it renders ipv6 headline', async () => { const ip = 'fe80--24ce-f7ff-fede-a571'; - (useWithSource as jest.Mock).mockReturnValue({ + (useSourcererScope as jest.Mock).mockReturnValue({ indicesExist: true, indexPattern: {}, }); diff --git a/x-pack/plugins/security_solution/public/network/pages/details/index.tsx b/x-pack/plugins/security_solution/public/network/pages/details/index.tsx index 085cddf53ff65..eaeb31c020473 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/index.tsx @@ -24,7 +24,6 @@ import { IpOverview } from '../../components/details'; import { SiemSearchBar } from '../../../common/components/search_bar'; import { WrapperPage } from '../../../common/components/wrapper_page'; import { useNetworkDetails } from '../../containers/details'; -import { useWithSource } from '../../../common/containers/source'; import { FlowTargetSourceDest, LastEventIndexKey } from '../../../graphql/types'; import { useKibana } from '../../../common/lib/kibana'; import { decodeIpv6 } from '../../../common/lib/helpers'; @@ -44,6 +43,7 @@ import { AnomaliesQueryTabBody } from '../../../common/containers/anomalies/anom import { esQuery } from '../../../../../../../src/plugins/data/public'; import { networkModel } from '../../store'; import { SecurityPageName } from '../../../app/types'; +import { useSourcererScope } from '../../../common/containers/sourcerer'; export { getBreadcrumbs } from './utils'; const NetworkDetailsManage = manageQuery(IpOverview); @@ -83,7 +83,7 @@ const NetworkDetailsComponent: React.FC = () => { dispatch(setNetworkDetailsTablesActivePageToZero()); }, [detailName, dispatch]); - const { docValueFields, indicesExist, indexPattern } = useWithSource(); + const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope(); const ip = decodeIpv6(detailName); const filterQuery = convertToBuildEsQuery({ config: esQuery.getEsQueryConfig(uiSettings), @@ -96,6 +96,7 @@ const NetworkDetailsComponent: React.FC = () => { docValueFields, skip: isInitializing, filterQuery, + indexNames: selectedPatterns, ip, }); @@ -124,7 +125,14 @@ const NetworkDetailsComponent: React.FC = () => { border data-test-subj="network-details-headline" draggableArguments={headerDraggableArguments} - subtitle={} + subtitle={ + + } title={ip} > @@ -155,6 +163,7 @@ const NetworkDetailsComponent: React.FC = () => { endDate={to} filterQuery={filterQuery} flowTarget={FlowTargetSourceDest.source} + indexNames={selectedPatterns} ip={ip} skip={isInitializing} startDate={from} @@ -169,6 +178,7 @@ const NetworkDetailsComponent: React.FC = () => { endDate={to} flowTarget={FlowTargetSourceDest.destination} filterQuery={filterQuery} + indexNames={selectedPatterns} ip={ip} skip={isInitializing} startDate={from} @@ -187,6 +197,7 @@ const NetworkDetailsComponent: React.FC = () => { endDate={to} filterQuery={filterQuery} flowTarget={FlowTargetSourceDest.source} + indexNames={selectedPatterns} ip={ip} skip={isInitializing} startDate={from} @@ -201,6 +212,7 @@ const NetworkDetailsComponent: React.FC = () => { endDate={to} flowTarget={FlowTargetSourceDest.destination} filterQuery={filterQuery} + indexNames={selectedPatterns} ip={ip} skip={isInitializing} startDate={from} @@ -217,6 +229,7 @@ const NetworkDetailsComponent: React.FC = () => { endDate={to} filterQuery={filterQuery} flowTarget={flowTarget} + indexNames={selectedPatterns} ip={ip} skip={isInitializing} startDate={from} @@ -229,6 +242,7 @@ const NetworkDetailsComponent: React.FC = () => { { endDate={to} filterQuery={filterQuery} flowTarget={(flowTarget as unknown) as FlowTargetSourceDest} + indexNames={selectedPatterns} ip={ip} setQuery={setQuery} skip={isInitializing} @@ -257,6 +272,7 @@ const NetworkDetailsComponent: React.FC = () => { startDate={from} endDate={to} skip={isInitializing} + indexNames={selectedPatterns} ip={ip} type={type} flowTarget={flowTarget} diff --git a/x-pack/plugins/security_solution/public/network/pages/details/network_http_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/network_http_query_table.tsx index 1b1b2b5f4f46e..0a88519390486 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/network_http_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/network_http_query_table.tsx @@ -28,6 +28,7 @@ export const NetworkHttpQueryTable = ({ ] = useNetworkHttp({ endDate, filterQuery, + indexNames: [], ip, skip, startDate, diff --git a/x-pack/plugins/security_solution/public/network/pages/details/network_top_countries_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/network_top_countries_query_table.tsx index 42ddd3a6bb4a4..8a7d499a8ef5f 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/network_top_countries_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/network_top_countries_query_table.tsx @@ -31,6 +31,7 @@ export const NetworkTopCountriesQueryTable = ({ endDate, flowTarget, filterQuery, + indexNames: [], ip, skip, startDate, diff --git a/x-pack/plugins/security_solution/public/network/pages/details/network_top_n_flow_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/network_top_n_flow_query_table.tsx index 821452201b78b..d56d6d4f6b3ee 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/network_top_n_flow_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/network_top_n_flow_query_table.tsx @@ -18,6 +18,7 @@ export const NetworkTopNFlowQueryTable = ({ filterQuery, flowTarget, ip, + indexNames, setQuery, skip, startDate, @@ -30,6 +31,7 @@ export const NetworkTopNFlowQueryTable = ({ endDate, filterQuery, flowTarget, + indexNames, ip, skip, startDate, diff --git a/x-pack/plugins/security_solution/public/network/pages/details/tls_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/tls_query_table.tsx index 5184fccecf07a..b8c53cdf10fee 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/tls_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/tls_query_table.tsx @@ -30,6 +30,7 @@ export const TlsQueryTable = ({ endDate, filterQuery, flowTarget, + indexNames: [], ip, skip, startDate, diff --git a/x-pack/plugins/security_solution/public/network/pages/details/types.ts b/x-pack/plugins/security_solution/public/network/pages/details/types.ts index 960df0d5e36b9..3b5a7fab3c6e7 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/details/types.ts @@ -22,6 +22,7 @@ export interface OwnProps { endDate: string; filterQuery: string | ESTermQuery; ip: string; + indexNames: string[]; skip: boolean; setQuery: GlobalTimeArgs['setQuery']; } diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx index 1e57ca42257e7..1c61760d9845f 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx @@ -19,6 +19,7 @@ const NetworkTopCountriesTableManage = manageQuery(NetworkTopCountriesTable); export const CountriesQueryTabBody = ({ endDate, filterQuery, + indexNames, skip, startDate, setQuery, @@ -32,6 +33,7 @@ export const CountriesQueryTabBody = ({ endDate, flowTarget, filterQuery, + indexNames, skip, startDate, type: networkModel.NetworkType.page, diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx index 5adb78edbec8e..a8bae2509e0d6 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx @@ -45,6 +45,7 @@ const DnsQueryTabBodyComponent: React.FC = ({ deleteQuery, endDate, filterQuery, + indexNames, skip, startDate, setQuery, @@ -64,6 +65,7 @@ const DnsQueryTabBodyComponent: React.FC = ({ ] = useNetworkDns({ endDate, filterQuery, + indexNames, skip, startDate, type, @@ -88,6 +90,7 @@ const DnsQueryTabBodyComponent: React.FC = ({ endDate={endDate} filterQuery={filterQuery} id={HISTOGRAM_ID} + indexNames={indexNames} setQuery={setQuery} showLegend={true} startDate={startDate} diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx index 3caff05734c1e..85d6b6daabd6c 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx @@ -19,6 +19,7 @@ const NetworkHttpTableManage = manageQuery(NetworkHttpTable); export const HttpQueryTabBody = ({ endDate, filterQuery, + indexNames, skip, startDate, setQuery, @@ -29,6 +30,7 @@ export const HttpQueryTabBody = ({ ] = useNetworkHttp({ endDate, filterQuery, + indexNames, skip, startDate, type: networkModel.NetworkType.page, diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/ips_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/ips_query_tab_body.tsx index c83bf6ff80901..465b3347ce707 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/ips_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/ips_query_tab_body.tsx @@ -19,6 +19,7 @@ const NetworkTopNFlowTableManage = manageQuery(NetworkTopNFlowTable); export const IPsQueryTabBody = ({ endDate, filterQuery, + indexNames, skip, startDate, setQuery, @@ -31,6 +32,7 @@ export const IPsQueryTabBody = ({ endDate, flowTarget, filterQuery, + indexNames, skip, startDate, type: networkModel.NetworkType.page, diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx index 2da56a30df7c7..7af474728c824 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx @@ -33,6 +33,7 @@ export const NetworkRoutes = React.memo( isInitializing, from, indexPattern, + indexNames, setQuery, setAbsoluteRangeDatePicker, }) => { @@ -83,6 +84,7 @@ export const NetworkRoutes = React.memo( const commonProps = { startDate: from, endDate: to, + indexNames, skip: isInitializing, type, narrowDateRange, diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx index 279891cc181e3..702a9e696220f 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx @@ -16,6 +16,7 @@ const TlsQueryTabBodyComponent: React.FC = ({ endDate, filterQuery, flowTarget, + indexNames, ip = '', setQuery, skip, @@ -29,6 +30,7 @@ const TlsQueryTabBodyComponent: React.FC = ({ endDate, filterQuery, flowTarget, + indexNames, ip, skip, startDate, diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts b/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts index 2ef04d3371c0b..ed04fd01a7b89 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts @@ -22,6 +22,7 @@ interface QueryTabBodyProps extends Pick ({ +const mockProps = { networkPagePath: '', to, from, @@ -69,40 +69,42 @@ const getMockProps = () => ({ setQuery: jest.fn(), capabilitiesFetched: true, hasMlUserPermissions: true, -}); - +}; +const mockUseSourcererScope = useSourcererScope as jest.Mock; describe('rendering - rendering', () => { - test('it renders the Setup Instructions text when no index is available', async () => { - (useWithSource as jest.Mock).mockReturnValue({ + test('it renders the Setup Instructions text when no index is available', () => { + mockUseSourcererScope.mockReturnValue({ + selectedPatterns: [], indicesExist: false, }); const wrapper = mount( - + ); expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); }); - test('it DOES NOT render the Setup Instructions text when an index is available', async () => { - (useWithSource as jest.Mock).mockReturnValue({ + test('it DOES NOT render the Setup Instructions text when an index is available', () => { + mockUseSourcererScope.mockReturnValue({ + selectedPatterns: [], indicesExist: true, indexPattern: {}, }); const wrapper = mount( - + ); expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(false); }); - test('it should add the new filters after init', async () => { + test('it should add the new filters after init', () => { const newFilters: Filter[] = [ { query: { @@ -134,7 +136,8 @@ describe('rendering - rendering', () => { }, }, ]; - (useWithSource as jest.Mock).mockReturnValue({ + mockUseSourcererScope.mockReturnValue({ + selectedPatterns: [], indicesExist: true, indexPattern: { fields: [], title: 'title' }, }); @@ -150,7 +153,7 @@ describe('rendering - rendering', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index 8aed6385ea24d..6aea771e49499 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -24,7 +24,6 @@ import { SiemSearchBar } from '../../common/components/search_bar'; import { WrapperPage } from '../../common/components/wrapper_page'; import { useFullScreen } from '../../common/containers/use_full_screen'; import { useGlobalTime } from '../../common/containers/use_global_time'; -import { useWithSource } from '../../common/containers/source'; import { LastEventIndexKey } from '../../graphql/types'; import { useKibana } from '../../common/lib/kibana'; import { convertToBuildEsQuery } from '../../common/lib/keury'; @@ -44,8 +43,7 @@ import { timelineSelectors } from '../../timelines/store/timeline'; import { TimelineId } from '../../../common/types/timeline'; import { timelineDefaults } from '../../timelines/store/timeline/defaults'; import { TimelineModel } from '../../timelines/store/timeline/model'; - -const sourceId = 'default'; +import { useSourcererScope } from '../../common/containers/sourcerer'; const NetworkComponent = React.memo( ({ @@ -84,7 +82,7 @@ const NetworkComponent = React.memo( [setAbsoluteRangeDatePicker] ); - const { indicesExist, indexPattern } = useWithSource(sourceId); + const { docValueFields, indicesExist, indexPattern, selectedPatterns } = useSourcererScope(); const filterQuery = convertToBuildEsQuery({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), indexPattern, @@ -111,7 +109,13 @@ const NetworkComponent = React.memo( } + subtitle={ + + } title={i18n.PAGE_TITLE} /> @@ -127,11 +131,12 @@ const NetworkComponent = React.memo( @@ -150,6 +155,7 @@ const NetworkComponent = React.memo( from={from} isInitializing={isInitializing} indexPattern={indexPattern} + indexNames={selectedPatterns} setQuery={setQuery} setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} type={networkModel.NetworkType.page} diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx index e365ac38d31df..6f1b7e95e763d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx @@ -29,7 +29,15 @@ const to = '2019-03-31T06:00:00.000Z'; describe('Alerts by category', () => { let wrapper: ReactWrapper; - + const testProps = { + deleteQuery: jest.fn(), + filters: [], + from, + indexNames: [], + indexPattern: mockIndexPattern, + setQuery: jest.fn(), + to, + }; describe('before loading data', () => { beforeAll(async () => { (useMatrixHistogram as jest.Mock).mockReturnValue([ @@ -44,14 +52,7 @@ describe('Alerts by category', () => { wrapper = mount( - + ); @@ -119,14 +120,7 @@ describe('Alerts by category', () => { wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx index 1a2238c763bda..4d3b2dbf3f11f 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx @@ -43,6 +43,7 @@ interface Props extends Pick = ({ from, hideHeaderChildren = false, indexPattern, + indexNames, query = DEFAULT_QUERY, setQuery, to, @@ -117,6 +119,7 @@ const AlertsByCategoryComponent: React.FC = ({ })} headerChildren={hideHeaderChildren ? null : alertsCountViewAlertsButton} id={ID} + indexNames={indexNames} setQuery={setQuery} startDate={from} {...alertsByCategoryHistogramConfigs} diff --git a/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx index f2d6b50326082..44cb7a65dbc5e 100644 --- a/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx @@ -20,11 +20,16 @@ describe('EventCounts', () => { const from = '2020-01-20T20:49:57.080Z'; const to = '2020-01-21T20:49:57.080Z'; + const testProps = { + from, + indexNames: [], + indexPattern: mockIndexPattern, + setQuery: jest.fn(), + to, + }; + test('it filters the `Host events` widget with a `host.name` `exists` filter', () => { - const wrapper = mount( - , - { wrappingComponent: TestProviders } - ); + const wrapper = mount(, { wrappingComponent: TestProviders }); expect( (wrapper.find('Memo(OverviewHostComponent)').first().props() as OverviewHostProps).filterQuery @@ -32,10 +37,7 @@ describe('EventCounts', () => { }); test('it filters the `Network events` widget with a `source.ip` or `destination.ip` `exists` filter', () => { - const wrapper = mount( - , - { wrappingComponent: TestProviders } - ); + const wrapper = mount(, { wrappingComponent: TestProviders }); expect( (wrapper.find('Memo(OverviewNetworkComponent)').first().props() as OverviewNetworkProps) diff --git a/x-pack/plugins/security_solution/public/overview/components/event_counts/index.tsx b/x-pack/plugins/security_solution/public/overview/components/event_counts/index.tsx index 23f5998f44111..6e47de68221c7 100644 --- a/x-pack/plugins/security_solution/public/overview/components/event_counts/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/event_counts/index.tsx @@ -31,6 +31,7 @@ const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; interface Props extends Pick { filters?: Filter[]; + indexNames: string[]; indexPattern: IIndexPattern; query?: Query; } @@ -38,6 +39,7 @@ interface Props extends Pick { const EventCountsComponent: React.FC = ({ filters = NO_FILTERS, from, + indexNames, indexPattern, query = DEFAULT_QUERY, setQuery, @@ -56,6 +58,7 @@ const EventCountsComponent: React.FC = ({ queries: [query], filters: [...filters, ...filterHostData], })} + indexNames={indexNames} startDate={from} setQuery={setQuery} /> @@ -72,6 +75,7 @@ const EventCountsComponent: React.FC = ({ queries: [query], filters: [...filters, ...filterNetworkData], })} + indexNames={indexNames} startDate={from} setQuery={setQuery} /> diff --git a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx index 7025afde963f1..1fa3d8f4ef27a 100644 --- a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx @@ -47,7 +47,7 @@ interface Props extends Pick = ({ from, headerChildren, indexPattern, - indexToAdd, + indexNames, onlyField, query = DEFAULT_QUERY, setAbsoluteRangeDatePickerTarget, @@ -164,7 +164,7 @@ const EventsByDatasetComponent: React.FC = ({ filterQuery={filterQuery} headerChildren={headerContent} id={uniqueQueryId} - indexToAdd={indexToAdd} + indexNames={indexNames} setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} setQuery={setQuery} showSpacer={showSpacer} diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/overview/components/host_overview/__snapshots__/index.test.tsx.snap index c9c34682519e2..47d45ab740dcf 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/__snapshots__/index.test.tsx.snap @@ -192,8 +192,10 @@ exports[`Host Summary Component rendering it renders the default Host Summary 1` }, } } + docValueFields={Array []} endDate="2019-06-18T06:00:00.000Z" id="hostOverview" + indexNames={Array []} isLoadingAnomaliesData={false} loading={false} narrowDateRange={[MockFunction]} diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx index 6bd0390d014a3..69bd053d707b9 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx @@ -20,8 +20,10 @@ describe('Host Summary Component', () => { ( ({ anomaliesData, data, + docValueFields, endDate, id, isLoadingAnomaliesData, + indexNames, loading, narrowDateRange, startDate, @@ -91,7 +95,9 @@ export const HostOverview = React.memo( description: data.host != null && data.host.name && data.host.name.length ? ( ) : ( @@ -103,7 +109,9 @@ export const HostOverview = React.memo( description: data.host != null && data.host.name && data.host.name.length ? ( ) : ( @@ -111,7 +119,7 @@ export const HostOverview = React.memo( ), }, ], - [data] + [data, docValueFields, indexNames] ); const firstColumn = useMemo( () => diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx index b932add7afc2c..8f0e9a56254ec 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx @@ -27,7 +27,12 @@ jest.mock('../../../common/components/link_to'); const startDate = '2020-01-20T20:49:57.080Z'; const endDate = '2020-01-21T20:49:57.080Z'; - +const testProps = { + endDate, + indexNames: [], + setQuery: jest.fn(), + startDate, +}; const MOCKED_RESPONSE = { overviewHost: { auditbeatAuditd: 1, @@ -79,7 +84,7 @@ describe('OverviewHost', () => { test('it renders the expected widget title', () => { const wrapper = mount( - + ); @@ -92,7 +97,7 @@ describe('OverviewHost', () => { useHostOverviewMock.mockReturnValueOnce([true, { overviewHost: {} }]); const wrapper = mount( - + ); @@ -102,7 +107,7 @@ describe('OverviewHost', () => { test('it renders the expected event count in the subtitle after loading events', async () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx index 3f35d0abbaa85..f92f004bd2448 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx @@ -22,11 +22,11 @@ import { InspectButtonContainer } from '../../../common/components/inspect'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { SecurityPageName } from '../../../app/types'; import { LinkButton } from '../../../common/components/links'; -import { Sourcerer } from '../../../common/components/sourcerer'; export interface OwnProps { startDate: GlobalTimeArgs['from']; endDate: GlobalTimeArgs['to']; + indexNames: string[]; filterQuery?: ESQuery | string; setQuery: GlobalTimeArgs['setQuery']; } @@ -37,6 +37,7 @@ export type OverviewHostProps = OwnProps; const OverviewHostComponent: React.FC = ({ endDate, filterQuery, + indexNames, startDate, setQuery, }) => { @@ -47,6 +48,7 @@ const OverviewHostComponent: React.FC = ({ const [loading, { overviewHost, id, inspect, refetch }] = useHostOverview({ endDate, filterQuery, + indexNames, startDate, }); @@ -109,10 +111,7 @@ const OverviewHostComponent: React.FC = ({ /> } > - <> - - {hostPageButton} - + <>{hostPageButton} { const startDate = '2020-01-20T20:49:57.080Z'; const endDate = '2020-01-21T20:49:57.080Z'; +const defaultProps = { + endDate, + startDate, + setQuery: jest.fn(), + indexNames: [], +}; const MOCKED_RESPONSE = { overviewNetwork: { @@ -88,7 +94,7 @@ describe('OverviewNetwork', () => { test('it renders the expected widget title', () => { const wrapper = mount( - + ); @@ -101,7 +107,7 @@ describe('OverviewNetwork', () => { useNetworkOverviewMock.mockReturnValueOnce([true, { overviewNetwork: {} }]); const wrapper = mount( - + ); @@ -111,7 +117,7 @@ describe('OverviewNetwork', () => { test('it renders the expected event count in the subtitle after loading events', async () => { const wrapper = mount( - + ); @@ -123,7 +129,7 @@ describe('OverviewNetwork', () => { it('it renders View Network', () => { const wrapper = mount( - + ); @@ -133,7 +139,7 @@ describe('OverviewNetwork', () => { it('when click on View Network we call navigateToApp to make sure to navigate to right page', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx index 089bed3c67808..178a752d1286f 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx @@ -30,6 +30,7 @@ export interface OverviewNetworkProps { startDate: GlobalTimeArgs['from']; endDate: GlobalTimeArgs['to']; filterQuery?: ESQuery | string; + indexNames: string[]; setQuery: GlobalTimeArgs['setQuery']; } @@ -38,6 +39,7 @@ const OverviewNetworkStatsManage = manageQuery(OverviewNetworkStats); const OverviewNetworkComponent: React.FC = ({ endDate, filterQuery, + indexNames, startDate, setQuery, }) => { @@ -48,6 +50,7 @@ const OverviewNetworkComponent: React.FC = ({ const [loading, { overviewNetwork, id, inspect, refetch }] = useNetworkOverview({ endDate, filterQuery, + indexNames, startDate, }); diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx index 446679ae26d9e..8a0d2f4f16202 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.test.tsx @@ -33,7 +33,7 @@ describe('RecentCases', () => { wrapper.find(`[data-test-subj="no-cases-create-case"]`).first().simulate('click'); expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: - "/create?timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "/create?sourcerer=(default:!('apm-*-transaction*','auditbeat-*','endgame-*','filebeat-*','logs-*','packetbeat-*','winlogbeat-*'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", }); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx index 75ab85fe0c429..ac439107cb4a5 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx @@ -5,10 +5,9 @@ */ import { noop } from 'lodash/fp'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import deepEqual from 'fast-deep-equal'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { HostsQueries, HostOverviewRequestOptions, @@ -18,8 +17,6 @@ import { useKibana } from '../../../common/lib/kibana'; import { inputsModel } from '../../../common/store/inputs'; import { createFilter } from '../../../common/containers/helpers'; import { ESQuery } from '../../../../common/typed_json'; -import { useManageSource } from '../../../common/containers/sourcerer'; -import { SOURCERER_FEATURE_FLAG_ON } from '../../../common/containers/sourcerer/constants'; import { AbortError, isCompleteResponse, @@ -42,6 +39,7 @@ export interface HostOverviewArgs { interface UseHostOverview { filterQuery?: ESQuery | string; endDate: string; + indexNames: string[]; skip?: boolean; startDate: string; } @@ -49,23 +47,16 @@ interface UseHostOverview { export const useHostOverview = ({ filterQuery, endDate, + indexNames, skip = false, startDate, }: UseHostOverview): [boolean, HostOverviewArgs] => { - const { data, notifications, uiSettings } = useKibana().services; - const { activeSourceGroupId, getManageSourceGroupById } = useManageSource(); - const { indexPatterns } = useMemo(() => getManageSourceGroupById(activeSourceGroupId), [ - getManageSourceGroupById, - activeSourceGroupId, - ]); - const uiDefaultIndexPatterns = uiSettings.get(DEFAULT_INDEX_KEY); - const defaultIndex = SOURCERER_FEATURE_FLAG_ON ? indexPatterns : uiDefaultIndexPatterns; - + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const [loading, setLoading] = useState(false); const [overviewHostRequest, setHostRequest] = useState({ - defaultIndex, + defaultIndex: indexNames, factoryQueryType: HostsQueries.overview, filterQuery: createFilter(filterQuery), timerange: { @@ -145,7 +136,7 @@ export const useHostOverview = ({ setHostRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, filterQuery: createFilter(filterQuery), timerange: { interval: '12h', @@ -158,7 +149,7 @@ export const useHostOverview = ({ } return prevRequest; }); - }, [defaultIndex, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, skip, startDate]); useEffect(() => { overviewHostSearch(overviewHostRequest); diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx index ae1fe942d8403..588fb1f08ef6f 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx @@ -8,7 +8,6 @@ import { noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; import deepEqual from 'fast-deep-equal'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { NetworkQueries, NetworkOverviewRequestOptions, @@ -40,6 +39,7 @@ export interface NetworkOverviewArgs { interface UseNetworkOverview { filterQuery?: ESQuery | string; endDate: string; + indexNames: string[]; skip?: boolean; startDate: string; } @@ -47,16 +47,16 @@ interface UseNetworkOverview { export const useNetworkOverview = ({ filterQuery, endDate, + indexNames, skip = false, startDate, }: UseNetworkOverview): [boolean, NetworkOverviewArgs] => { - const { data, notifications, uiSettings } = useKibana().services; - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); const [loading, setLoading] = useState(false); const [overviewNetworkRequest, setNetworkRequest] = useState({ - defaultIndex, + defaultIndex: indexNames, factoryQueryType: NetworkQueries.overview, filterQuery: createFilter(filterQuery), timerange: { @@ -136,7 +136,7 @@ export const useNetworkOverview = ({ setNetworkRequest((prevRequest) => { const myRequest = { ...prevRequest, - defaultIndex, + defaultIndex: indexNames, filterQuery: createFilter(filterQuery), timerange: { interval: '12h', @@ -149,7 +149,7 @@ export const useNetworkOverview = ({ } return prevRequest; }); - }, [defaultIndex, endDate, filterQuery, skip, startDate]); + }, [indexNames, endDate, filterQuery, skip, startDate]); useEffect(() => { overviewNetworkSearch(overviewNetworkRequest); diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index 74225c4e4f823..222b9e008ddd7 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -10,16 +10,18 @@ import { MemoryRouter } from 'react-router-dom'; import '../../common/mock/match_media'; import { TestProviders } from '../../common/mock'; -import { useWithSource } from '../../common/containers/source'; import { useMessagesStorage, UseMessagesStorage, } from '../../common/containers/local_storage/use_messages_storage'; import { Overview } from './index'; import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; +import { useSourcererScope } from '../../common/containers/sourcerer'; +import { useFetchIndex } from '../../common/containers/source'; jest.mock('../../common/lib/kibana'); jest.mock('../../common/containers/source'); +jest.mock('../../common/containers/sourcerer'); jest.mock('../../common/containers/use_global_time', () => ({ useGlobalTime: jest.fn().mockReturnValue({ from: '2020-07-07T08:20:18.966Z', @@ -49,65 +51,29 @@ const endpointNoticeMessage = (hasMessageValue: boolean) => { clearAllMessages: () => undefined, }; }; - +const mockUseSourcererScope = useSourcererScope as jest.Mock; +const mockUseIngestEnabledCheck = useIngestEnabledCheck as jest.Mock; +const mockUseFetchIndex = useFetchIndex as jest.Mock; +const mockUseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; describe('Overview', () => { + beforeEach(() => { + mockUseFetchIndex.mockReturnValue([ + false, + { + indexExists: true, + }, + ]); + }); describe('rendering', () => { - describe('when no index is available', () => { - beforeEach(() => { - (useWithSource as jest.Mock).mockReturnValue({ - indicesExist: false, - }); - (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: false }); - const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock< - UseMessagesStorage - >; - mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); - }); - - it('renders the Setup Instructions text', () => { - const wrapper = mount( - - - - - - ); - expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); - }); - - it('does not show Endpoint get ready button when ingest is not enabled', () => { - const wrapper = mount( - - - - - - ); - expect(wrapper.find('[data-test-subj="empty-page-endpoint-action"]').exists()).toBe(false); - }); - - it('shows Endpoint get ready button when ingest is enabled', () => { - (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); - const wrapper = mount( - - - - - - ); - expect(wrapper.find('[data-test-subj="empty-page-endpoint-action"]').exists()).toBe(true); - }); - }); - - it('it DOES NOT render the Getting started text when an index is available', () => { - (useWithSource as jest.Mock).mockReturnValue({ + test('it DOES NOT render the Getting started text when an index is available', () => { + mockUseSourcererScope.mockReturnValue({ + selectedPatterns: [], indicesExist: true, indexPattern: {}, }); - const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; - mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); - (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); + mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -122,19 +88,20 @@ describe('Overview', () => { }); test('it DOES render the Endpoint banner when the endpoint index is NOT available AND storage is NOT set', () => { - (useWithSource as jest.Mock).mockReturnValueOnce({ + mockUseFetchIndex.mockReturnValue([ + false, + { + indexExists: false, + }, + ]); + mockUseSourcererScope.mockReturnValue({ + selectedPatterns: [], indicesExist: true, indexPattern: {}, }); - (useWithSource as jest.Mock).mockReturnValueOnce({ - indicesExist: false, - indexPattern: {}, - }); - - const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; - mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); - (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); + mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -149,19 +116,20 @@ describe('Overview', () => { }); test('it does NOT render the Endpoint banner when the endpoint index is NOT available but storage is set', () => { - (useWithSource as jest.Mock).mockReturnValueOnce({ + mockUseFetchIndex.mockReturnValue([ + false, + { + indexExists: false, + }, + ]); + mockUseSourcererScope.mockReturnValueOnce({ + selectedPatterns: [], indicesExist: true, indexPattern: {}, }); - (useWithSource as jest.Mock).mockReturnValueOnce({ - indicesExist: false, - indexPattern: {}, - }); - - const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; - mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); - (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); + mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); + mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -176,14 +144,14 @@ describe('Overview', () => { }); test('it does NOT render the Endpoint banner when the endpoint index is available AND storage is set', () => { - (useWithSource as jest.Mock).mockReturnValue({ - indicesExist: true, + mockUseSourcererScope.mockReturnValue({ + selectedPatterns: [], + indexExists: true, indexPattern: {}, }); - const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; - mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); - (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); + mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); + mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -198,14 +166,14 @@ describe('Overview', () => { }); test('it does NOT render the Endpoint banner when an index IS available but storage is NOT set', () => { - (useWithSource as jest.Mock).mockReturnValue({ + mockUseSourcererScope.mockReturnValue({ + selectedPatterns: [], indicesExist: true, indexPattern: {}, }); - const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; - mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); - (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true }); + mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: true }); const wrapper = mount( @@ -214,19 +182,20 @@ describe('Overview', () => { ); + wrapper.update(); expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false); wrapper.unmount(); }); test('it does NOT render the Endpoint banner when Ingest is NOT available', () => { - (useWithSource as jest.Mock).mockReturnValue({ + mockUseSourcererScope.mockReturnValue({ + selectedPatterns: [], indicesExist: true, indexPattern: {}, }); - const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; - mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); - (useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: false }); + mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true)); + mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: false }); const wrapper = mount( @@ -239,5 +208,50 @@ describe('Overview', () => { expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false); wrapper.unmount(); }); + + describe('when no index is available', () => { + beforeEach(() => { + mockUseSourcererScope.mockReturnValue({ + selectedPatterns: [], + indicesExist: false, + }); + mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: false }); + mockUseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false)); + }); + + it('renders the Setup Instructions text', () => { + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="empty-page"]').exists()).toBe(true); + }); + + it('does not show Endpoint get ready button when ingest is not enabled', () => { + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="empty-page-endpoint-action"]').exists()).toBe(false); + }); + + it('shows Endpoint get ready button when ingest is enabled', () => { + mockUseIngestEnabledCheck.mockReturnValue({ allEnabled: true }); + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="empty-page-endpoint-action"]').exists()).toBe(true); + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 520fd6c459705..5a3b4ec384686 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -15,7 +15,7 @@ import { FiltersGlobal } from '../../common/components/filters_global'; import { SiemSearchBar } from '../../common/components/search_bar'; import { WrapperPage } from '../../common/components/wrapper_page'; import { useGlobalTime } from '../../common/containers/use_global_time'; -import { useWithSource } from '../../common/containers/source'; +import { useFetchIndex } from '../../common/containers/source'; import { EventsByDataset } from '../components/events_by_dataset'; import { EventCounts } from '../components/event_counts'; @@ -30,6 +30,9 @@ import { EndpointNotice } from '../components/endpoint_notice'; import { useMessagesStorage } from '../../common/containers/local_storage/use_messages_storage'; import { ENDPOINT_METADATA_INDEX } from '../../../common/constants'; import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; +import { useSourcererScope } from '../../common/containers/sourcerer'; +import { Sourcerer } from '../../common/components/sourcerer'; +import { SourcererScopeName } from '../../common/store/sourcerer/model'; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; const NO_FILTERS: Filter[] = []; @@ -43,17 +46,13 @@ const OverviewComponent: React.FC = ({ query = DEFAULT_QUERY, setAbsoluteRangeDatePicker, }) => { + const { from, deleteQuery, setQuery, to } = useGlobalTime(); + const { indicesExist, indexPattern, selectedPatterns } = useSourcererScope(); + const endpointMetadataIndex = useMemo(() => { return [ENDPOINT_METADATA_INDEX]; }, []); - - const { from, deleteQuery, setQuery, to } = useGlobalTime(); - const { indicesExist, indexPattern } = useWithSource(); - const { indicesExist: metadataIndexExists } = useWithSource( - 'default', - endpointMetadataIndex, - true - ); + const [, { indexExists: metadataIndexExists }] = useFetchIndex(endpointMetadataIndex, true); const { addMessage, hasMessage } = useMessagesStorage(); const hasDismissEndpointNoticeMessage: boolean = useMemo( () => hasMessage('management', 'dismissEndpointNotice'), @@ -81,6 +80,7 @@ const OverviewComponent: React.FC = ({ )} + @@ -107,6 +107,7 @@ const OverviewComponent: React.FC = ({ filters={filters} from={from} indexPattern={indexPattern} + indexNames={selectedPatterns} query={query} setQuery={setQuery} to={to} @@ -119,6 +120,7 @@ const OverviewComponent: React.FC = ({ filters={filters} from={from} indexPattern={indexPattern} + indexNames={selectedPatterns} query={query} setQuery={setQuery} to={to} @@ -129,6 +131,7 @@ const OverviewComponent: React.FC = ({ { - const docLinks = useKibana().services.docLinks; - + const { docLinks, http } = useKibana().services; + const basePath = http.basePath.get(); return ( @@ -39,7 +39,7 @@ export const Summary = React.memo(() => { ), data: ( - + { private kibanaVersion: string; @@ -385,15 +390,32 @@ export class Plugin implements IPlugin( + { indices: defaultIndicesName, onlyCheckIfIndicesExist: false }, + { + strategy: 'securitySolutionIndexFields', + } + ) + .toPromise(), + ]); - const { - detectionsSubPlugin, - hostsSubPlugin, - networkSubPlugin, - timelinesSubPlugin, - managementSubPlugin, - } = await this.downloadSubPlugins(); const { apolloClient } = composeLibs(coreStart); const appLibs: AppObservableLibs = { apolloClient, kibana: coreStart }; const libs$ = new BehaviorSubject(appLibs); @@ -417,12 +439,18 @@ export class Plugin implements IPlugin( ({ onOpen, show, dataProviders, timelineId }) => { const badgeCount = useMemo(() => getBadgeCount(dataProviders), [dataProviders]); - const { browserFields } = useWithSource(); + const { browserFields } = useSourcererScope(SourcererScopeName.timeline); if (!show) { return null; diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx index fe0f0c8f8b91f..3814bc01bd9b1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.test.tsx @@ -33,33 +33,33 @@ describe('useTimelineManager', () => { expect(isStringifiedComparisonEqual(uninitializedTimeline, timelineDefaults)).toBeTruthy(); }); }); - - it('getIndexToAddById', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useTimelineManager() - ); - await waitForNextUpdate(); - const data = result.current.getIndexToAddById(testId); - expect(data).toEqual(timelineDefaults.indexToAdd); - }); - }); - - it('setIndexToAdd', async () => { - await act(async () => { - const indexToAddArgs = { id: testId, indexToAdd: ['example'] }; - const { result, waitForNextUpdate } = renderHook(() => - useTimelineManager() - ); - await waitForNextUpdate(); - result.current.initializeTimeline({ - id: testId, - }); - result.current.setIndexToAdd(indexToAddArgs); - const data = result.current.getIndexToAddById(testId); - expect(data).toEqual(indexToAddArgs.indexToAdd); - }); - }); + // TO DO sourcerer + // it('getIndexToAddById', async () => { + // await act(async () => { + // const { result, waitForNextUpdate } = renderHook(() => + // useTimelineManager() + // ); + // await waitForNextUpdate(); + // const data = result.current.getIndexToAddById(testId); + // expect(data).toEqual(timelineDefaults.indexToAdd); + // }); + // }); + // + // it('setIndexToAdd', async () => { + // await act(async () => { + // const indexToAddArgs = { id: testId, indexToAdd: ['example'] }; + // const { result, waitForNextUpdate } = renderHook(() => + // useTimelineManager() + // ); + // await waitForNextUpdate(); + // result.current.initializeTimeline({ + // id: testId, + // }); + // result.current.setIndexToAdd(indexToAddArgs); + // const data = result.current.getIndexToAddById(testId); + // expect(data).toEqual(indexToAddArgs.indexToAdd); + // }); + // }); it('setIsTimelineLoading', async () => { await act(async () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx index f82158fe65c11..4e1e877ec5b88 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx @@ -20,7 +20,6 @@ interface ManageTimelineInit { filterManager?: FilterManager; footerText?: string; id: string; - indexToAdd?: string[] | null; loadingText?: string; selectAll?: boolean; queryFields?: string[]; @@ -34,7 +33,6 @@ interface ManageTimeline { filterManager?: FilterManager; footerText: string; id: string; - indexToAdd: string[] | null; isLoading: boolean; loadingText: string; queryFields: string[]; @@ -58,11 +56,6 @@ type ActionManageTimeline = id: string; payload: boolean; } - | { - type: 'SET_INDEX_TO_ADD'; - id: string; - payload: string[]; - } | { type: 'SET_SELECT_ALL'; id: string; @@ -70,7 +63,6 @@ type ActionManageTimeline = }; export const getTimelineDefaults = (id: string) => ({ - indexToAdd: null, defaultModel: timelineDefaultModel, loadingText: i18n.LOADING_EVENTS, footerText: i18nF.TOTAL_COUNT_OF_EVENTS, @@ -96,14 +88,6 @@ const reducerManageTimeline = ( ...action.payload, }, } as ManageTimelineById; - case 'SET_INDEX_TO_ADD': - return { - ...state, - [action.id]: { - ...state[action.id], - indexToAdd: action.payload, - }, - } as ManageTimelineById; case 'SET_SELECT_ALL': return { ...state, @@ -127,12 +111,10 @@ const reducerManageTimeline = ( }; export interface UseTimelineManager { - getIndexToAddById: (id: string) => string[] | null; getManageTimelineById: (id: string) => ManageTimeline; getTimelineFilterManager: (id: string) => FilterManager | undefined; initializeTimeline: (newTimeline: ManageTimelineInit) => void; isManagedTimeline: (id: string) => boolean; - setIndexToAdd: (indexToAddArgs: { id: string; indexToAdd: string[] }) => void; setIsTimelineLoading: (isLoadingArgs: { id: string; isLoading: boolean }) => void; setSelectAll: (selectAllArgs: { id: string; selectAll: boolean }) => void; } @@ -163,14 +145,6 @@ export const useTimelineManager = ( [] ); - const setIndexToAdd = useCallback(({ id, indexToAdd }: { id: string; indexToAdd: string[] }) => { - dispatch({ - type: 'SET_INDEX_TO_ADD', - id, - payload: indexToAdd, - }); - }, []); - const setSelectAll = useCallback(({ id, selectAll }: { id: string; selectAll: boolean }) => { dispatch({ type: 'SET_SELECT_ALL', @@ -193,36 +167,23 @@ export const useTimelineManager = ( }, [initializeTimeline, state] ); - const getIndexToAddById = useCallback( - (id: string): string[] | null => { - if (state[id] != null) { - return state[id].indexToAdd; - } - return getTimelineDefaults(id).indexToAdd; - }, - [state] - ); const isManagedTimeline = useCallback((id: string): boolean => state[id] != null, [state]); return { - getIndexToAddById, getManageTimelineById, getTimelineFilterManager, initializeTimeline, isManagedTimeline, - setIndexToAdd, setIsTimelineLoading, setSelectAll, }; }; const init = { - getIndexToAddById: (id: string) => null, getManageTimelineById: (id: string) => getTimelineDefaults(id), getTimelineFilterManager: () => undefined, initializeTimeline: () => noop, isManagedTimeline: () => false, - setIndexToAdd: () => undefined, setIsTimelineLoading: () => noop, setSelectAll: () => noop, }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index ed44fc14e3efa..c89114ec77138 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -273,6 +273,7 @@ describe('helpers', () => { highlightedDropAndProviderId: '', historyIds: [], id: 'savedObject-1', + indexNames: [], isFavorite: false, isLive: false, isSelectAllChecked: false, @@ -371,6 +372,7 @@ describe('helpers', () => { highlightedDropAndProviderId: '', historyIds: [], id: 'savedObject-1', + indexNames: [], isFavorite: false, isLive: false, isSelectAllChecked: false, @@ -469,6 +471,7 @@ describe('helpers', () => { highlightedDropAndProviderId: '', historyIds: [], id: 'savedObject-1', + indexNames: [], isFavorite: false, isLive: false, isSelectAllChecked: false, @@ -564,6 +567,7 @@ describe('helpers', () => { filters: [], highlightedDropAndProviderId: '', historyIds: [], + indexNames: [], id: 'savedObject-1', isFavorite: false, isLive: false, @@ -699,6 +703,7 @@ describe('helpers', () => { filters: [], highlightedDropAndProviderId: '', historyIds: [], + indexNames: [], isFavorite: false, isLive: false, isSelectAllChecked: false, @@ -865,6 +870,7 @@ describe('helpers', () => { ], highlightedDropAndProviderId: '', historyIds: [], + indexNames: [], isFavorite: false, isLive: false, isSelectAllChecked: false, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index b6b6148340a4a..c89740f667b29 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -56,6 +56,8 @@ import { OpenTimelineResult, UpdateTimeline, DispatchUpdateTimeline } from './ty import { createNote } from '../notes/helpers'; import { IS_OPERATOR } from '../timeline/data_providers/data_provider'; import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range'; +import { sourcererActions } from '../../../common/store/sourcerer'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; export const OPEN_TIMELINE_CLASS_NAME = 'open-timeline'; @@ -375,6 +377,13 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli to, ruleNote, }: UpdateTimeline): (() => void) => () => { + dispatch( + sourcererActions.setSelectedIndexPatterns({ + id: SourcererScopeName.timeline, + selectedPatterns: timeline.indexNames, + eventType: timeline.eventType, + }) + ); dispatch(dispatchSetTimelineRangeDatePicker({ from, to })); dispatch(dispatchAddTimeline({ id, timeline, savedTimeline: duplicate })); if ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index f681043a9047d..dc824a8eb6272 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -5,12 +5,12 @@ */ import ApolloClient from 'apollo-client'; -import React, { useEffect, useState, useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import { connect, ConnectedProps, shallowEqual, useSelector } from 'react-redux'; import { Dispatch } from 'redux'; import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../../graphql/types'; -import { State } from '../../../common/store'; +import { sourcererSelectors, State } from '../../../common/store'; import { TimelineId } from '../../../../common/types/timeline'; import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model'; import { timelineSelectors } from '../../../timelines/store/timeline'; @@ -110,6 +110,15 @@ export const StatefulOpenTimelineComponent = React.memo( /** The requested field to sort on */ const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); + const existingIndexNamesSelector = useMemo( + () => sourcererSelectors.getAllExistingIndexNamesSelector(), + [] + ); + const existingIndexNames = useSelector( + existingIndexNamesSelector, + shallowEqual + ); + const { customTemplateTimelineCount, defaultTimelineCount, @@ -193,7 +202,12 @@ export const StatefulOpenTimelineComponent = React.memo( const deleteTimelines: DeleteTimelines = useCallback( async (timelineIds: string[]) => { if (timelineIds.includes(timeline.savedObjectId || '')) { - createNewTimeline({ id: TimelineId.active, columns: defaultHeaders, show: false }); + createNewTimeline({ + id: TimelineId.active, + columns: defaultHeaders, + indexNames: existingIndexNames, + show: false, + }); } await apolloClient.mutate< @@ -206,7 +220,7 @@ export const StatefulOpenTimelineComponent = React.memo( }); refetch(); }, - [apolloClient, createNewTimeline, refetch, timeline] + [apolloClient, createNewTimeline, existingIndexNames, refetch, timeline] ); const onDeleteOneTimeline: OnDeleteOneTimeline = useCallback( @@ -382,12 +396,14 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ createNewTimeline: ({ id, columns, + indexNames, show, }: { id: string; columns: ColumnHeaderOptions[]; + indexNames: string[]; show?: boolean; - }) => dispatch(dispatchCreateNewTimeline({ id, columns, show })), + }) => dispatch(dispatchCreateNewTimeline({ id, columns, indexNames, show })), updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => dispatch(dispatchUpdateIsLoading({ id, isLoading })), updateTimeline: dispatchUpdateTimeline(dispatch), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap index d76ddace40a5a..18a648f2abfaa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -806,9 +806,15 @@ In other use cases the message field can be used to concatenate different values } docValueFields={Array []} end="2018-03-24T03:33:52.253Z" - eventType="raw" filters={Array []} - id="foo" + id="test" + indexNames={ + Array [ + "filebeat-*", + "auditbeat-*", + "packetbeat-*", + ] + } indexPattern={ Object { "fields": Array [ @@ -900,9 +906,7 @@ In other use cases the message field can be used to concatenate different values "title": "filebeat-*,auditbeat-*,packetbeat-*", } } - indexToAdd={Array []} isLive={false} - isLoadingSource={false} isSaving={false} itemsPerPage={5} itemsPerPageOptions={ @@ -914,7 +918,7 @@ In other use cases the message field can be used to concatenate different values } kqlMode="search" kqlQueryExpression="" - loadingIndexName={false} + loadingSourcerer={false} onChangeItemsPerPage={[MockFunction]} onClose={[MockFunction]} onDataProviderEdited={[MockFunction]} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx index 824a37f72ba59..cf9fbfaf19326 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx @@ -11,12 +11,15 @@ import { useDispatch } from 'react-redux'; import { Ecs } from '../../../../../common/ecs'; import { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; import { updateTimelineGraphEventId } from '../../../store/timeline/actions'; -import { EventType } from '../../../store/timeline/model'; +import { + TimelineEventsType, + TimelineTypeLiteral, + TimelineType, +} from '../../../../../common/types/timeline'; import { OnPinEvent, OnUnPinEvent } from '../events'; import { ActionIconItem } from './actions/action_icon_item'; import * as i18n from './translations'; -import { TimelineTypeLiteral, TimelineType } from '../../../../../common/types/timeline'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const omitTypenameAndEmpty = (k: string, v: any): any | undefined => @@ -101,7 +104,7 @@ export const getEventIdToDataMapping = ( }, {}); /** Return eventType raw or signal */ -export const getEventType = (event: Ecs): Omit => { +export const getEventType = (event: Ecs): Omit => { if (!isEmpty(event.signal?.rule?.id)) { return 'signal'; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 58c87c086df6e..fc0bcb134158c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -10,7 +10,7 @@ import { inputsModel } from '../../../../common/store'; import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; import { TimelineItem, TimelineNonEcsData } from '../../../../../common/search_strategy'; import { Note } from '../../../../common/lib/note'; -import { ColumnHeaderOptions, EventType } from '../../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model'; import { AddNoteToEvent, UpdateNote } from '../../notes/helpers'; import { OnColumnRemoved, @@ -31,7 +31,7 @@ import { RowRenderer } from './renderers/row_renderer'; import { Sort } from './sort'; import { GraphOverlay } from '../../graph_overlay'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; -import { TimelineId, TimelineType } from '../../../../../common/types/timeline'; +import { TimelineEventsType, TimelineId, TimelineType } from '../../../../../common/types/timeline'; export interface BodyProps { addNoteToEvent: AddNoteToEvent; @@ -45,7 +45,7 @@ export interface BodyProps { isEventViewer?: boolean; isSelectAllChecked: boolean; eventIdToNoteIds: Readonly>; - eventType?: EventType; + eventType?: TimelineEventsType; loadingEventIds: Readonly; onColumnRemoved: OnColumnRemoved; onColumnResized: OnColumnResized; @@ -68,7 +68,7 @@ export interface BodyProps { updateNote: UpdateNote; } -export const hasAdditionalActions = (id: string, eventType?: EventType): boolean => +export const hasAdditionalActions = (id: string, eventType?: TimelineEventsType): boolean => id === TimelineId.detectionsPage || id === TimelineId.detectionsRulesDetailsPage || ((id === TimelineId.active && eventType && ['all', 'signal', 'alert'].includes(eventType)) ?? diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/helpers.test.tsx index 61b4c2b23c267..d8a9f7528ddae 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/helpers.test.tsx @@ -112,7 +112,7 @@ describe('helpers', () => { }) ).toEqual({ updatedDestinationGroup: [sourceGroup[moveProviderFromSourceIndex]], - updatedSourceGroup: sourceGroup.filter((_, i) => i !== moveProviderFromSourceIndex), + updatedSourcererScope: sourceGroup.filter((_, i) => i !== moveProviderFromSourceIndex), }); }) ); @@ -138,7 +138,7 @@ describe('helpers', () => { : [...acc, sourceGroup[moveProviderFromSourceIndex], p], [] ), - updatedSourceGroup: sourceGroup.filter((_, i) => i !== moveProviderFromSourceIndex), + updatedSourcererScope: sourceGroup.filter((_, i) => i !== moveProviderFromSourceIndex), }); }) ) @@ -171,7 +171,7 @@ describe('helpers', () => { p.id !== sourceGroup[moveProviderFromSourceIndex].id || i === moveProviderToDestinationIndex ), - updatedSourceGroup: sourceGroup.filter((_, i) => i !== moveProviderFromSourceIndex), + updatedSourcererScope: sourceGroup.filter((_, i) => i !== moveProviderFromSourceIndex), }); }) ) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/helpers.tsx index 923ef86c0bbc0..00c7f0705f3ce 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/helpers.tsx @@ -42,7 +42,7 @@ export const move = ({ sourceGroup: DataProvidersAnd[]; }): { updatedDestinationGroup: DataProvidersAnd[]; - updatedSourceGroup: DataProvidersAnd[]; + updatedSourcererScope: DataProvidersAnd[]; } => { const sourceClone = [...sourceGroup]; const destinationClone = [...destinationGroup]; @@ -56,7 +56,7 @@ export const move = ({ return { updatedDestinationGroup: deDuplicatedDestinationGroup, - updatedSourceGroup: sourceClone, + updatedSourcererScope: sourceClone, }; }; @@ -169,7 +169,7 @@ export const moveProvidersBetweenGroups = ({ const moveProviderFromSourceIndex = source.index; const moveProviderToDestinationIndex = destination.index; - const { updatedDestinationGroup, updatedSourceGroup } = move({ + const { updatedDestinationGroup, updatedSourcererScope } = move({ destinationGroup, moveProviderFromSourceIndex, moveProviderToDestinationIndex, @@ -180,7 +180,7 @@ export const moveProvidersBetweenGroups = ({ (acc, group, i) => [ ...acc, i === sourceGroupIndex - ? [...updatedSourceGroup] + ? [...updatedSourcererScope] : i === destinationGroupIndex ? [...updatedDestinationGroup] : [...group], diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index f1bfdd5d33606..d2737de7e75dc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -6,49 +6,32 @@ import { mount } from 'enzyme'; import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { act } from 'react-dom/test-utils'; import useResizeObserver from 'use-resize-observer/polyfilled'; import '../../../common/mock/match_media'; +import { mockBrowserFields, mockDocValueFields } from '../../../common/containers/source/mock'; + import { - useSignalIndex, - ReturnSignalIndex, -} from '../../../detections/containers/detection_engine/alerts/use_signal_index'; -import { mocksSource } from '../../../common/containers/source/mock'; -// we don't have the types for waitFor just yet, so using "as waitFor" until when we do -import { wait as waitFor } from '@testing-library/react'; -import { defaultHeaders, mockTimelineData, TestProviders } from '../../../common/mock'; -import { Direction } from '../../../graphql/types'; -import { timelineActions } from '../../store/timeline'; + mockIndexNames, + mockIndexPattern, + mockTimelineData, + TestProviders, +} from '../../../common/mock'; -import { Sort } from './body/sort'; -import { mockDataProviders } from './data_providers/mock/mock_data_providers'; -import { StatefulTimeline, Props as StatefulTimelineProps } from './index'; -import { Timeline } from './timeline'; -import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; +import { StatefulTimeline, OwnProps as StatefulTimelineOwnProps } from './index'; import { useTimelineEvents } from '../../containers/index'; jest.mock('../../containers/index', () => ({ useTimelineEvents: jest.fn(), })); -jest.mock('../../../common/lib/kibana', () => { - const originalModule = jest.requireActual('../../../common/lib/kibana'); - return { - ...originalModule, - useGetUserSavedObjectPermissions: jest.fn(), - }; -}); - +jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/components/url_state/normalize_time_range.ts'); const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); -const mockUseSignalIndex: jest.Mock = useSignalIndex as jest.Mock; -jest.mock('../../../detections/containers/detection_engine/alerts/use_signal_index'); jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -58,105 +41,37 @@ jest.mock('react-router-dom', () => { }; }); jest.mock('../flyout/header_with_close_button'); +jest.mock('../../../common/containers/sourcerer', () => { + const originalModule = jest.requireActual('../../../common/containers/sourcerer'); + + return { + ...originalModule, + useSourcererScope: jest.fn().mockReturnValue({ + browserFields: mockBrowserFields, + docValueFields: mockDocValueFields, + loading: false, + indexPattern: mockIndexPattern, + selectedPatterns: mockIndexNames, + }), + }; +}); describe('StatefulTimeline', () => { - let props = {} as StatefulTimelineProps; - const sort: Sort = { - columnId: '@timestamp', - sortDirection: Direction.desc, + const props: StatefulTimelineOwnProps = { + id: 'id', + onClose: jest.fn(), + usersViewing: [], }; - const startDate = '2018-03-23T18:49:23.132Z'; - const endDate = '2018-03-24T03:33:52.253Z'; - - const mocks = mocksSource; beforeEach(() => { (useTimelineEvents as jest.Mock).mockReturnValue([false, { events: mockTimelineData }]); - props = { - addProvider: timelineActions.addProvider, - columns: defaultHeaders, - createTimeline: timelineActions.createTimeline, - dataProviders: mockDataProviders, - eventType: 'raw', - end: endDate, - filters: [], - graphEventId: undefined, - id: 'foo', - isLive: false, - isSaving: false, - isTimelineExists: false, - itemsPerPage: 5, - itemsPerPageOptions: [5, 10, 20], - kqlMode: 'search', - kqlQueryExpression: '', - onClose: jest.fn(), - onDataProviderEdited: timelineActions.dataProviderEdited, - removeColumn: timelineActions.removeColumn, - removeProvider: timelineActions.removeProvider, - show: true, - showCallOutUnauthorizedMsg: false, - sort, - start: startDate, - status: TimelineStatus.active, - timelineType: TimelineType.default, - updateColumns: timelineActions.updateColumns, - updateDataProviderEnabled: timelineActions.updateDataProviderEnabled, - updateDataProviderExcluded: timelineActions.updateDataProviderExcluded, - updateDataProviderKqlQuery: timelineActions.updateDataProviderKqlQuery, - updateDataProviderType: timelineActions.updateDataProviderType, - updateHighlightedDropAndProviderId: timelineActions.updateHighlightedDropAndProviderId, - updateItemsPerPage: timelineActions.updateItemsPerPage, - updateItemsPerPageOptions: timelineActions.updateItemsPerPageOptions, - updateSort: timelineActions.updateSort, - upsertColumn: timelineActions.upsertColumn, - usersViewing: ['elastic'], - }; }); - describe('indexToAdd', () => { - test('Make sure that indexToAdd return an unknown index if signalIndex does not exist', async () => { - mockUseSignalIndex.mockImplementation(() => ({ - loading: false, - signalIndexExists: false, - signalIndexName: undefined, - })); - const wrapper = mount( - - - - - - ); - await act(async () => { - await waitFor(() => { - wrapper.update(); - const timeline = wrapper.find(Timeline); - expect(timeline.props().indexToAdd).toEqual([ - 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51', - ]); - }); - }); - }); - - test('Make sure that indexToAdd return siem signal index if signalIndex exist', async () => { - mockUseSignalIndex.mockImplementation(() => ({ - loading: false, - signalIndexExists: true, - signalIndexName: 'mock-siem-signals-index', - })); - const wrapper = mount( - - - - - - ); - await act(async () => { - await waitFor(() => { - wrapper.update(); - const timeline = wrapper.find(Timeline); - expect(timeline.props().indexToAdd).toEqual(['mock-siem-signals-index']); - }); - }); - }); + test('renders ', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="timeline"]')).toBeTruthy(); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index c170c93ee6083..ccdb0793bc03f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -5,13 +5,10 @@ */ import { isEmpty } from 'lodash/fp'; -import React, { useEffect, useCallback, useMemo } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; -import { NO_ALERT_INDEX } from '../../../../common/constants'; -import { useWithSource } from '../../../common/containers/source'; -import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; import { inputsModel, inputsSelectors, State } from '../../../common/store'; import { timelineActions, timelineSelectors } from '../../store/timeline'; import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model'; @@ -26,6 +23,8 @@ import { OnToggleDataProviderType, } from './events'; import { Timeline } from './timeline'; +import { useSourcererScope } from '../../../common/containers/sourcerer'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; export interface OwnProps { id: string; @@ -40,7 +39,6 @@ const StatefulTimelineComponent = React.memo( columns, createTimeline, dataProviders, - eventType, end, filters, graphEventId, @@ -69,19 +67,13 @@ const StatefulTimelineComponent = React.memo( upsertColumn, usersViewing, }) => { - const { loading, signalIndexExists, signalIndexName } = useSignalIndex(); - - const indexToAdd = useMemo(() => { - if ( - eventType && - signalIndexExists && - signalIndexName != null && - ['signal', 'alert', 'all'].includes(eventType) - ) { - return [signalIndexName]; - } - return [NO_ALERT_INDEX]; // Following index does not exist so we won't show any events; - }, [eventType, signalIndexExists, signalIndexName]); + const { + browserFields, + docValueFields, + loading, + indexPattern, + selectedPatterns, + } = useSourcererScope(SourcererScopeName.timeline); const onDataProviderRemoved: OnDataProviderRemoved = useCallback( (providerId: string, andProviderId?: string) => @@ -160,22 +152,16 @@ const StatefulTimelineComponent = React.memo( }); } }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [columns, id] + [columns, id, removeColumn, upsertColumn] ); useEffect(() => { if (createTimeline != null && !isTimelineExists) { - createTimeline({ id, columns: defaultHeaders, show: false }); + createTimeline({ id, columns: defaultHeaders, indexNames: selectedPatterns, show: false }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const { docValueFields, indexPattern, browserFields, loading: isLoadingSource } = useWithSource( - 'default', - indexToAdd - ); - return ( ( dataProviders={dataProviders!} docValueFields={docValueFields} end={end} - eventType={eventType} filters={filters} graphEventId={graphEventId} id={id} indexPattern={indexPattern} - indexToAdd={indexToAdd} + indexNames={selectedPatterns} isLive={isLive} - isLoadingSource={isLoadingSource} isSaving={isSaving} itemsPerPage={itemsPerPage!} itemsPerPageOptions={itemsPerPageOptions!} kqlMode={kqlMode} kqlQueryExpression={kqlQueryExpression} - loadingIndexName={loading} + loadingSourcerer={loading} onChangeItemsPerPage={onChangeItemsPerPage} onClose={onClose} onDataProviderEdited={onDataProviderEditedLocal} @@ -215,10 +199,8 @@ const StatefulTimelineComponent = React.memo( /> ); }, - // eslint-disable-next-line complexity (prevProps, nextProps) => { return ( - prevProps.eventType === nextProps.eventType && prevProps.end === nextProps.end && prevProps.graphEventId === nextProps.graphEventId && prevProps.id === nextProps.id && diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx index 5b3bc72fc37ca..7da3cf940da50 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx @@ -93,15 +93,18 @@ describe('useCreateTimelineButton', () => { wrapper.find('[data-test-subj="timeline-new"]').first().simulate('click'); expect(mockDispatch.mock.calls[0][0].type).toEqual( - 'x-pack/security_solution/local/timeline/CREATE_TIMELINE' + 'x-pack/security_solution/local/sourcerer/SET_SELECTED_INDEX_PATTERNS' ); expect(mockDispatch.mock.calls[1][0].type).toEqual( - 'x-pack/security_solution/local/inputs/ADD_GLOBAL_LINK_TO' + 'x-pack/security_solution/local/timeline/CREATE_TIMELINE' ); expect(mockDispatch.mock.calls[2][0].type).toEqual( - 'x-pack/security_solution/local/inputs/ADD_TIMELINE_LINK_TO' + 'x-pack/security_solution/local/inputs/ADD_GLOBAL_LINK_TO' ); expect(mockDispatch.mock.calls[3][0].type).toEqual( + 'x-pack/security_solution/local/inputs/ADD_TIMELINE_LINK_TO' + ); + expect(mockDispatch.mock.calls[4][0].type).toEqual( 'x-pack/security_solution/local/inputs/SET_RELATIVE_RANGE_DATE_PICKER' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx index 97f3b1df011ff..3919ee21b2a90 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import React, { useCallback, useMemo } from 'react'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { timelineActions } from '../../../store/timeline'; @@ -15,6 +15,9 @@ import { TimelineTypeLiteral, } from '../../../../../common/types/timeline'; import { inputsActions, inputsSelectors } from '../../../../common/store/inputs'; +import { sourcererActions, sourcererSelectors } from '../../../../common/store/sourcerer'; +import { State } from '../../../../common/store'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; export const useCreateTimelineButton = ({ timelineId, @@ -26,6 +29,11 @@ export const useCreateTimelineButton = ({ closeGearMenu?: () => void; }) => { const dispatch = useDispatch(); + const existingIndexNamesSelector = useMemo( + () => sourcererSelectors.getAllExistingIndexNamesSelector(), + [] + ); + const existingIndexNames = useSelector(existingIndexNamesSelector, shallowEqual); const { timelineFullScreen, setTimelineFullScreen } = useFullScreen(); const globalTimeRange = useSelector(inputsSelectors.globalTimeRangeSelector); const createTimeline = useCallback( @@ -33,12 +41,19 @@ export const useCreateTimelineButton = ({ if (id === TimelineId.active && timelineFullScreen) { setTimelineFullScreen(false); } + dispatch( + sourcererActions.setSelectedIndexPatterns({ + id: SourcererScopeName.timeline, + selectedPatterns: existingIndexNames, + }) + ); dispatch( timelineActions.createTimeline({ id, columns: defaultHeaders, show, timelineType, + indexNames: existingIndexNames, }) ); dispatch(inputsActions.addGlobalLinkTo({ linkToId: 'timeline' })); @@ -59,7 +74,14 @@ export const useCreateTimelineButton = ({ ); } }, - [dispatch, globalTimeRange, setTimelineFullScreen, timelineFullScreen, timelineType] + [ + existingIndexNames, + dispatch, + globalTimeRange, + setTimelineFullScreen, + timelineFullScreen, + timelineType, + ] ); const handleButtonClick = useCallback(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx index 7ee7e12c0ef62..166705128ce02 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/index.tsx @@ -24,11 +24,14 @@ import { inputsModel, inputsSelectors, } from '../../../../common/store'; +import { TimelineEventsType } from '../../../../../common/types/timeline'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; -import { KqlMode, TimelineModel, EventType } from '../../../../timelines/store/timeline/model'; +import { KqlMode, TimelineModel } from '../../../../timelines/store/timeline/model'; import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; import { dispatchUpdateReduxTime } from '../../../../common/components/super_date_picker'; import { SearchOrFilter } from './search_or_filter'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { sourcererActions } from '../../../../common/store/sourcerer'; interface OwnProps { browserFields: BrowserFields; @@ -62,7 +65,7 @@ const StatefulSearchOrFilterComponent = React.memo( timelineId, to, toStr, - updateEventType, + updateEventTypeAndIndexesName, updateKqlMode, updateReduxTime, }) => { @@ -111,13 +114,14 @@ const StatefulSearchOrFilterComponent = React.memo( [timelineId, setSavedQueryId] ); - const handleUpdateEventType = useCallback( - (newEventType: EventType) => - updateEventType({ + const handleUpdateEventTypeAndIndexesName = useCallback( + (newEventType: TimelineEventsType, indexNames: string[]) => + updateEventTypeAndIndexesName({ id: timelineId, eventType: newEventType, + indexNames, }), - [timelineId, updateEventType] + [timelineId, updateEventTypeAndIndexesName] ); return ( @@ -143,7 +147,7 @@ const StatefulSearchOrFilterComponent = React.memo( timelineId={timelineId} to={to} toStr={toStr} - updateEventType={handleUpdateEventType} + updateEventTypeAndIndexesName={handleUpdateEventTypeAndIndexesName} updateKqlMode={updateKqlMode!} updateReduxTime={updateReduxTime} /> @@ -211,8 +215,24 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ filterQuery, }) ), - updateEventType: ({ id, eventType }: { id: string; eventType: EventType }) => - dispatch(timelineActions.updateEventType({ id, eventType })), + updateEventTypeAndIndexesName: ({ + id, + eventType, + indexNames, + }: { + id: string; + eventType: TimelineEventsType; + indexNames: string[]; + }) => { + dispatch(timelineActions.updateEventType({ id, eventType })); + dispatch(timelineActions.updateIndexNames({ id, indexNames })); + dispatch( + sourcererActions.setSelectedIndexPatterns({ + id: SourcererScopeName.timeline, + selectedPatterns: indexNames, + }) + ); + }, updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => dispatch(timelineActions.updateKqlMode({ id, kqlMode })), setKqlFilterQueryDraft: ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx index fc2bd1c21abdc..16200f4e5ef9a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx @@ -4,17 +4,47 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiHealth, EuiSuperSelect } from '@elastic/eui'; -import React, { memo } from 'react'; +import { + EuiAccordion, + EuiButton, + EuiButtonEmpty, + EuiRadioGroup, + EuiComboBox, + EuiComboBoxOptionOption, + EuiHealth, + EuiIcon, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiPopoverTitle, + EuiSpacer, + EuiText, + EuiToolTip, +} from '@elastic/eui'; +import deepEqual from 'fast-deep-equal'; +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; import styled from 'styled-components'; -import { EventType } from '../../../../timelines/store/timeline/model'; +import { State } from '../../../../common/store'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { TimelineEventsType } from '../../../../../common/types/timeline'; +import { getSourcererScopeSelector, SourcererScopeSelector } from './selectors'; import * as i18n from './translations'; -interface EventTypeOptionItem { - value: EventType; - inputDisplay: React.ReactElement; -} +const PopoverContent = styled.div` + width: 600px; +`; + +const ResetButton = styled(EuiButtonEmpty)` + width: fit-content; +`; + +const MyEuiButton = styled(EuiButton)` + .euiHealth { + vertical-align: middle; + } +`; const AllEuiHealth = styled(EuiHealth)` margin-left: -2px; @@ -36,6 +66,18 @@ const WarningEuiHealth = styled(EuiHealth)` } `; +const AdvancedSettings = styled(EuiText)` + color: ${({ theme }) => theme.eui.euiColorPrimary}; +`; + +const ConfigHelper = styled(EuiText)` + margin-left: 4px; +`; + +const Filter = styled(EuiRadioGroup)` + margin-left: 4px; +`; + const PickEventContainer = styled.div` .euiSuperSelect { width: 170px; @@ -46,43 +88,309 @@ const PickEventContainer = styled.div` } `; -export const eventTypeOptions: EventTypeOptionItem[] = [ +const getEventTypeOptions = (isCustomDisabled: boolean = true) => [ { - value: 'all', - inputDisplay: ( + id: 'all', + label: ( {i18n.ALL_EVENT} ), }, { - value: 'raw', - inputDisplay: {i18n.RAW_EVENT}, + id: 'raw', + label: {i18n.RAW_EVENT}, }, { - value: 'alert', - inputDisplay: {i18n.DETECTION_ALERTS_EVENT}, + id: 'alert', + label: {i18n.DETECTION_ALERTS_EVENT}, + }, + { + id: 'custom', + label: <>{i18n.CUSTOM_INDEX_PATTERNS}, + disabled: isCustomDisabled, }, ]; interface PickEventTypeProps { - eventType: EventType; - onChangeEventType: (value: EventType) => void; + eventType: TimelineEventsType; + onChangeEventTypeAndIndexesName: (value: TimelineEventsType, indexNames: string[]) => void; } const PickEventTypeComponents: React.FC = ({ - eventType, - onChangeEventType, + eventType = 'all', + onChangeEventTypeAndIndexesName, }) => { + const [isPopoverOpen, setPopover] = useState(false); + const [showAdvanceSettings, setAdvanceSettings] = useState(eventType === 'custom'); + const [filterEventType, setFilterEventType] = useState(eventType); + const sourcererScopeSelector = useMemo(getSourcererScopeSelector, []); + const { configIndexPatterns, kibanaIndexPatterns, signalIndexName, sourcererScope } = useSelector< + State, + SourcererScopeSelector + >((state) => sourcererScopeSelector(state, SourcererScopeName.timeline), deepEqual); + const [selectedOptions, setSelectedOptions] = useState>>( + sourcererScope.selectedPatterns.map((indexSelected) => ({ + label: indexSelected, + value: indexSelected, + })) + ); + + const indexesPatternOptions = useMemo( + () => + [ + ...configIndexPatterns, + ...kibanaIndexPatterns.map((kip) => kip.title), + signalIndexName, + ].reduce>>((acc, index) => { + if (index != null && !acc.some((o) => o.label.includes(index))) { + return [...acc, { label: index, value: index }]; + } + return acc; + }, []), + [configIndexPatterns, kibanaIndexPatterns, signalIndexName] + ); + + const renderOption = useCallback( + (option) => { + const { value } = option; + if (kibanaIndexPatterns.some((kip) => kip.title === value)) { + return ( + <> + {value} + + ); + } + return <>{value}; + }, + [kibanaIndexPatterns] + ); + + const onChangeCombo = useCallback( + (newSelectedOptions: Array>) => { + const localSelectedPatterns = newSelectedOptions.map((nso) => nso.label); + if ( + localSelectedPatterns.sort().join() === + [...configIndexPatterns, signalIndexName].sort().join() + ) { + setFilterEventType('all'); + } else if (localSelectedPatterns.sort().join() === configIndexPatterns.sort().join()) { + setFilterEventType('raw'); + } else if (localSelectedPatterns.sort().join() === signalIndexName) { + setFilterEventType('alert'); + } else { + setFilterEventType('custom'); + } + + setSelectedOptions(newSelectedOptions); + }, + [configIndexPatterns, signalIndexName] + ); + + const onChangeFilter = useCallback( + (filter) => { + setFilterEventType(filter); + if (filter === 'all') { + setSelectedOptions( + [...configIndexPatterns, signalIndexName ?? ''].map((indexSelected) => ({ + label: indexSelected, + value: indexSelected, + })) + ); + } else if (filter === 'raw') { + setSelectedOptions( + configIndexPatterns.map((indexSelected) => ({ + label: indexSelected, + value: indexSelected, + })) + ); + } else if (filter === 'alert') { + setSelectedOptions([ + { + label: signalIndexName ?? '', + value: signalIndexName ?? '', + }, + ]); + } else if (filter === 'kibana') { + setSelectedOptions( + kibanaIndexPatterns.map((kip) => ({ + label: kip.title, + value: kip.title, + })) + ); + } + }, + [configIndexPatterns, kibanaIndexPatterns, signalIndexName] + ); + + const togglePopover = useCallback( + () => setPopover((prevIsPopoverOpen) => !prevIsPopoverOpen), + [] + ); + + const closePopover = useCallback(() => setPopover(false), []); + + const handleSaveIndices = useCallback(() => { + onChangeEventTypeAndIndexesName( + filterEventType, + selectedOptions.map((so) => so.label) + ); + setPopover(false); + }, [filterEventType, onChangeEventTypeAndIndexesName, selectedOptions]); + + const resetDataSources = useCallback(() => { + setSelectedOptions( + sourcererScope.selectedPatterns.map((indexSelected) => ({ + label: indexSelected, + value: indexSelected, + })) + ); + setFilterEventType(eventType); + }, [eventType, sourcererScope.selectedPatterns]); + + const comboBox = useMemo( + () => ( + + ), + [onChangeCombo, indexesPatternOptions, renderOption, selectedOptions] + ); + + const filterOptions = useMemo(() => getEventTypeOptions(filterEventType !== 'custom'), [ + filterEventType, + ]); + + const filter = useMemo( + () => ( + + ), + [filterEventType, filterOptions, onChangeFilter] + ); + + const button = useMemo(() => { + const options = getEventTypeOptions(); + return ( + + {options.find((opt) => opt.id === eventType)?.label} + + ); + }, [eventType, sourcererScope.loading, togglePopover]); + + const tooltipContent = useMemo( + () => (isPopoverOpen ? null : sourcererScope.selectedPatterns.sort().join(', ')), + [isPopoverOpen, sourcererScope.selectedPatterns] + ); + + const ButtonContent = useMemo( + () => ( + + {showAdvanceSettings + ? i18n.HIDE_INDEX_PATTERNS_ADVANCED_SETTINGS + : i18n.SHOW_INDEX_PATTERNS_ADVANCED_SETTINGS} + + ), + [showAdvanceSettings] + ); + + useEffect(() => { + const newSelectedOptions = sourcererScope.selectedPatterns.map((indexSelected) => ({ + label: indexSelected, + value: indexSelected, + })); + setSelectedOptions((prevSelectedOptions) => { + if (!deepEqual(newSelectedOptions, prevSelectedOptions)) { + return newSelectedOptions; + } + return prevSelectedOptions; + }); + }, [sourcererScope.selectedPatterns]); + + useEffect(() => { + setFilterEventType((prevFilter) => (prevFilter !== eventType ? eventType : prevFilter)); + setAdvanceSettings(eventType === 'custom'); + }, [eventType]); + return ( - + + + + + <>{i18n.SELECT_INDEX_PATTERNS} + + + {filter} + + + <> + + {comboBox} + + + {!showAdvanceSettings && ( + <> + + + {i18n.CONFIGURE_INDEX_PATTERNS} + + + )} + + + + + {i18n.DATA_SOURCES_RESET} + + + + + {i18n.SAVE_INDEX_PATTERNS} + + + + + + ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx index e04cef4ad8d93..32a516497f607 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx @@ -15,7 +15,8 @@ import { } from '../../../../../../../../src/plugins/data/public'; import { BrowserFields } from '../../../../common/containers/source'; import { KueryFilterQuery, KueryFilterQueryKind } from '../../../../common/store'; -import { KqlMode, EventType } from '../../../../timelines/store/timeline/model'; +import { TimelineEventsType } from '../../../../../common/types/timeline'; +import { KqlMode } from '../../../../timelines/store/timeline/model'; import { DispatchUpdateReduxTime } from '../../../../common/components/super_date_picker'; import { DataProvider } from '../data_providers/data_provider'; import { QueryBarTimeline } from '../query_bar'; @@ -47,7 +48,7 @@ interface Props { applyKqlFilterQuery: (expression: string, kind: KueryFilterQueryKind) => void; browserFields: BrowserFields; dataProviders: DataProvider[]; - eventType: EventType; + eventType: TimelineEventsType; filterManager: FilterManager; filterQuery: KueryFilterQuery; filterQueryDraft: KueryFilterQuery; @@ -66,7 +67,7 @@ interface Props { savedQueryId: string | null; to: string; toStr: string; - updateEventType: (eventType: EventType) => void; + updateEventTypeAndIndexesName: (eventType: TimelineEventsType, indexNames: string[]) => void; updateReduxTime: DispatchUpdateReduxTime; } @@ -114,7 +115,7 @@ export const SearchOrFilter = React.memo( setSavedQueryId, to, toStr, - updateEventType, + updateEventTypeAndIndexesName, updateKqlMode, updateReduxTime, }) => { @@ -167,7 +168,10 @@ export const SearchOrFilter = React.memo( /> - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/selectors.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/selectors.tsx new file mode 100644 index 0000000000000..2fdcf7a0eb0c1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/selectors.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { State } from '../../../../common/store'; +import { sourcererSelectors } from '../../../../common/store/selectors'; +import { + KibanaIndexPatterns, + ManageScope, + SourcererScopeName, +} from '../../../../common/store/sourcerer/model'; + +export interface SourcererScopeSelector { + configIndexPatterns: string[]; + kibanaIndexPatterns: KibanaIndexPatterns; + signalIndexName: string | null; + sourcererScope: ManageScope; +} + +export const getSourcererScopeSelector = () => { + const getkibanaIndexPatternsSelector = sourcererSelectors.kibanaIndexPatternsSelector(); + const getScopesSelector = sourcererSelectors.scopesSelector(); + const getConfigIndexPatternsSelector = sourcererSelectors.configIndexPatternsSelector(); + const getSignalIndexNameSelector = sourcererSelectors.signalIndexNameSelector(); + + const mapStateToProps = (state: State, scopeId: SourcererScopeName): SourcererScopeSelector => { + const kibanaIndexPatterns = getkibanaIndexPatternsSelector(state); + const scope = getScopesSelector(state)[scopeId]; + const configIndexPatterns = getConfigIndexPatternsSelector(state); + const signalIndexName = getSignalIndexNameSelector(state); + + return { + kibanaIndexPatterns, + configIndexPatterns, + signalIndexName, + sourcererScope: scope, + }; + }; + + return mapStateToProps; +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts index b5c78c458697c..f595881a57d03 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/translations.ts @@ -73,14 +73,14 @@ export const FILTER_OR_SEARCH_WITH_KQL = i18n.translate( export const ALL_EVENT = i18n.translate( 'xpack.securitySolution.timeline.searchOrFilter.eventTypeAllEvent', { - defaultMessage: 'All', + defaultMessage: 'All data sources', } ); export const RAW_EVENT = i18n.translate( 'xpack.securitySolution.timeline.searchOrFilter.eventTypeRawEvent', { - defaultMessage: 'Raw events', + defaultMessage: 'Events', } ); @@ -90,3 +90,59 @@ export const DETECTION_ALERTS_EVENT = i18n.translate( defaultMessage: 'Detection Alerts', } ); + +export const CUSTOM_INDEX_PATTERNS = i18n.translate( + 'xpack.securitySolution.timeline.searchOrFilter.customeIndexNames', + { + defaultMessage: 'Custom', + } +); + +export const SELECT_INDEX_PATTERNS = i18n.translate( + 'xpack.securitySolution.timeline.searchOrFilter.indexPatterns.help', + { + defaultMessage: 'Data sources selection', + } +); + +export const CONFIGURE_INDEX_PATTERNS = i18n.translate( + 'xpack.securitySolution.timeline.searchOrFilter.indexPatterns.configure', + { + defaultMessage: 'View data sources associated with each of the above selections', + } +); + +export const SAVE_INDEX_PATTERNS = i18n.translate( + 'xpack.securitySolution.timeline.searchOrFilter.indexPatterns.save', + { + defaultMessage: 'Save', + } +); + +export const SHOW_INDEX_PATTERNS_ADVANCED_SETTINGS = i18n.translate( + 'xpack.securitySolution.timeline.searchOrFilter.indexPatterns.showAdvancedSettings', + { + defaultMessage: 'Show Advanced', + } +); + +export const HIDE_INDEX_PATTERNS_ADVANCED_SETTINGS = i18n.translate( + 'xpack.securitySolution.timeline.searchOrFilter.indexPatterns.hideAdvancedSettings', + { + defaultMessage: 'Hide Advanced', + } +); + +export const DATA_SOURCES_RESET = i18n.translate( + 'xpack.securitySolution.timeline.searchOrFilter.indexPatterns.resetSettings', + { + defaultMessage: 'Reset', + } +); + +export const PICK_INDEX_PATTERNS = i18n.translate( + 'xpack.securitySolution.timeline.searchOrFilter.indexPatterns.pickIndexPatternsCombo', + { + defaultMessage: 'Pick index patterns', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx index 234814a68877d..e898779eacce9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -8,7 +8,7 @@ import { EuiLoadingSpinner } from '@elastic/eui'; import { rgba } from 'polished'; import styled, { createGlobalStyle } from 'styled-components'; -import { EventType } from '../../../timelines/store/timeline/model'; +import { TimelineEventsType } from '../../../../common/types/timeline'; import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '../../../common/components/drag_and_drop/helpers'; /** @@ -173,7 +173,7 @@ export const EventsTbody = styled.div.attrs(({ className = '' }) => ({ export const EventsTrGroup = styled.div.attrs(({ className = '' }) => ({ className: `siemEventsTable__trGroup ${className}`, -}))<{ className?: string; eventType: Omit; showLeftBorder: boolean }>` +}))<{ className?: string; eventType: Omit; showLeftBorder: boolean }>` border-bottom: ${({ theme }) => theme.eui.euiBorderWidthThin} solid ${({ theme }) => theme.eui.euiColorLightShade}; ${({ theme, eventType, showLeftBorder }) => diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx index 887a6a546d2b7..bde1e7bf5829a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx @@ -10,7 +10,12 @@ import useResizeObserver from 'use-resize-observer/polyfilled'; import { mockBrowserFields } from '../../../common/containers/source/mock'; import { Direction } from '../../../graphql/types'; -import { defaultHeaders, mockTimelineData, mockIndexPattern } from '../../../common/mock'; +import { + defaultHeaders, + mockTimelineData, + mockIndexPattern, + mockIndexNames, +} from '../../../common/mock'; import '../../../common/mock/match_media'; import { TestProviders } from '../../../common/mock/test_providers'; @@ -23,7 +28,7 @@ import { TimelineComponent, Props as TimelineComponentProps } from './timeline'; import { Sort } from './body/sort'; import { mockDataProviders } from './data_providers/mock/mock_data_providers'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; -import { TimelineStatus, TimelineType } from '../../../../common/types/timeline'; +import { TimelineId, TimelineStatus, TimelineType } from '../../../../common/types/timeline'; import { useTimelineEvents } from '../../containers/index'; import { useTimelineEventsDetails } from '../../containers/details/index'; @@ -94,22 +99,20 @@ describe('Timeline', () => { props = { browserFields: mockBrowserFields, columns: defaultHeaders, - id: 'foo', dataProviders: mockDataProviders, docValueFields: [], end: endDate, - eventType: 'raw' as TimelineComponentProps['eventType'], filters: [], + id: TimelineId.test, + indexNames: mockIndexNames, indexPattern, - indexToAdd: [], isLive: false, - isLoadingSource: false, isSaving: false, itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], kqlMode: 'search' as TimelineComponentProps['kqlMode'], kqlQueryExpression: '', - loadingIndexName: false, + loadingSourcerer: false, onChangeItemsPerPage: jest.fn(), onClose: jest.fn(), onDataProviderEdited: jest.fn(), @@ -119,12 +122,12 @@ describe('Timeline', () => { onToggleDataProviderType: jest.fn(), show: true, showCallOutUnauthorizedMsg: false, - start: startDate, sort, + start: startDate, status: TimelineStatus.active, + timelineType: TimelineType.default, toggleColumn: jest.fn(), usersViewing: ['elastic'], - timelineType: TimelineType.default, }; }); @@ -174,7 +177,7 @@ describe('Timeline', () => { test('it does NOT render the timeline table when the source is loading', () => { const wrapper = mount( - + ); @@ -211,16 +214,6 @@ describe('Timeline', () => { expect(wrapper.find('[data-test-subj="table-pagination"]').exists()).toEqual(false); }); - test('it defaults to showing `All`', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="pick-event-type"] button').text()).toEqual('All'); - }); - it('it shows the timeline footer', () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx index 434c7075a9470..d1a25e6f3e1a4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.tsx @@ -14,7 +14,7 @@ import { BrowserFields, DocValueFields } from '../../../common/containers/source import { Direction } from '../../../../common/search_strategy'; import { useTimelineEvents } from '../../containers/index'; import { useKibana } from '../../../common/lib/kibana'; -import { ColumnHeaderOptions, KqlMode, EventType } from '../../../timelines/store/timeline/model'; +import { ColumnHeaderOptions, KqlMode } from '../../../timelines/store/timeline/model'; import { defaultHeaders } from './body/column_headers/default_headers'; import { Sort } from './body/sort'; import { StatefulBody } from './body/stateful_body'; @@ -99,20 +99,18 @@ export interface Props { dataProviders: DataProvider[]; docValueFields: DocValueFields[]; end: string; - eventType?: EventType; filters: Filter[]; graphEventId?: string; id: string; + indexNames: string[]; indexPattern: IIndexPattern; - indexToAdd: string[]; isLive: boolean; - isLoadingSource: boolean; isSaving: boolean; itemsPerPage: number; itemsPerPageOptions: number[]; kqlMode: KqlMode; kqlQueryExpression: string; - loadingIndexName: boolean; + loadingSourcerer: boolean; onChangeItemsPerPage: OnChangeItemsPerPage; onClose: () => void; onDataProviderEdited: OnDataProviderEdited; @@ -122,12 +120,12 @@ export interface Props { onToggleDataProviderType: OnToggleDataProviderType; show: boolean; showCallOutUnauthorizedMsg: boolean; - start: string; sort: Sort; + start: string; status: TimelineStatusLiteral; + timelineType: TimelineType; toggleColumn: (column: ColumnHeaderOptions) => void; usersViewing: string[]; - timelineType: TimelineType; } /** The parent Timeline component */ @@ -137,20 +135,18 @@ export const TimelineComponent: React.FC = ({ dataProviders, docValueFields, end, - eventType, filters, graphEventId, id, indexPattern, - indexToAdd, + indexNames, isLive, - isLoadingSource, + loadingSourcerer, isSaving, itemsPerPage, itemsPerPageOptions, kqlMode, kqlQueryExpression, - loadingIndexName, onChangeItemsPerPage, onClose, onDataProviderEdited, @@ -204,11 +200,11 @@ export const TimelineComponent: React.FC = ({ const canQueryTimeline = useMemo( () => combinedQueries != null && - isLoadingSource != null && - !isLoadingSource && + loadingSourcerer != null && + !loadingSourcerer && !isEmpty(start) && !isEmpty(end), - [isLoadingSource, combinedQueries, start, end] + [loadingSourcerer, combinedQueries, start, end] ); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const timelineQueryFields = useMemo(() => { @@ -223,16 +219,13 @@ export const TimelineComponent: React.FC = ({ [sort.columnId, sort.sortDirection] ); const [isQueryLoading, setIsQueryLoading] = useState(false); - const { initializeTimeline, setIndexToAdd, setIsTimelineLoading } = useManageTimeline(); - + const { initializeTimeline, setIsTimelineLoading } = useManageTimeline(); useEffect(() => { initializeTimeline({ filterManager, id, - indexToAdd, }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [initializeTimeline, filterManager, id]); const [ loading, @@ -240,24 +233,19 @@ export const TimelineComponent: React.FC = ({ ] = useTimelineEvents({ docValueFields, endDate: end, - eventType, id, - indexToAdd, + indexNames, fields: timelineQueryFields, limit: itemsPerPage, filterQuery: combinedQueries?.filterQuery ?? '', startDate: start, - skip: canQueryTimeline, + skip: !canQueryTimeline, sort: timelineQuerySortField, }); useEffect(() => { - setIsTimelineLoading({ id, isLoading: isQueryLoading || loadingIndexName }); - }, [loadingIndexName, id, isQueryLoading, setIsTimelineLoading]); - - useEffect(() => { - setIndexToAdd({ id, indexToAdd }); - }, [id, indexToAdd, setIndexToAdd]); + setIsTimelineLoading({ id, isLoading: isQueryLoading || loadingSourcerer }); + }, [loadingSourcerer, id, isQueryLoading, setIsTimelineLoading]); useEffect(() => { setIsQueryLoading(loading); @@ -329,7 +317,7 @@ export const TimelineComponent: React.FC = ({ height={footerHeight} id={id} isLive={isLive} - isLoading={loading || loadingIndexName} + isLoading={loading || loadingSourcerer} itemsCount={events.length} itemsPerPage={itemsPerPage} itemsPerPageOptions={itemsPerPageOptions} diff --git a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx index 23c05805a5aa4..cd72ffb8ac803 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx @@ -9,7 +9,6 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import deepEqual from 'fast-deep-equal'; import { inputsModel } from '../../../common/store'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { useKibana } from '../../../common/lib/kibana'; import { DocValueFields, @@ -36,10 +35,9 @@ export const useTimelineEventsDetails = ({ eventId, skip, }: UseTimelineEventsDetailsProps): [boolean, EventsArgs['detailsData']] => { - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); const [loading, setLoading] = useState(false); const [ timelineDetailsRequest, @@ -102,7 +100,6 @@ export const useTimelineEventsDetails = ({ setTimelineDetailsRequest((prevRequest) => { const myRequest = { ...(prevRequest ?? {}), - defaultIndex, docValueFields, indexName, eventId, @@ -113,7 +110,7 @@ export const useTimelineEventsDetails = ({ } return prevRequest; }); - }, [defaultIndex, docValueFields, eventId, indexName, skip]); + }, [docValueFields, eventId, indexName, skip]); useEffect(() => { if (timelineDetailsRequest) { diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index d56a601fda4a3..54db52b985c31 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -10,18 +10,11 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { ESQuery } from '../../../common/typed_json'; -import { - IIndexPattern, - isCompleteResponse, - isErrorResponse, -} from '../../../../../../src/plugins/data/public'; - -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { isCompleteResponse, isErrorResponse } from '../../../../../../src/plugins/data/public'; import { inputsModel } from '../../common/store'; import { useKibana } from '../../common/lib/kibana'; import { createFilter } from '../../common/containers/helpers'; import { DocValueFields } from '../../common/containers/query_template'; -import { EventType } from '../../timelines/store/timeline/model'; import { timelineActions } from '../../timelines/store/timeline'; import { detectionsTimelineIds, skipQueryForDetectionsPage } from './helpers'; import { getInspectResponse } from '../../helpers'; @@ -58,15 +51,12 @@ interface UseTimelineEventsProps { filterQuery?: ESQuery | string; skip?: boolean; endDate: string; - eventType?: EventType; id: string; fields: string[]; - indexPattern?: IIndexPattern; - indexToAdd?: string[]; + indexNames: string[]; limit: number; sort: SortField; startDate: string; - canQueryTimeline?: boolean; } const getTimelineEvents = (timelineEdges: TimelineEdges[]): TimelineItem[] => @@ -77,10 +67,8 @@ const ID = 'timelineEventsQuery'; export const useTimelineEvents = ({ docValueFields, endDate, - eventType = 'raw', id = ID, - indexPattern, - indexToAdd = [], + indexNames, fields, filterQuery, startDate, @@ -89,41 +77,37 @@ export const useTimelineEvents = ({ field: '@timestamp', direction: Direction.asc, }, - canQueryTimeline = true, + skip = false, }: UseTimelineEventsProps): [boolean, TimelineArgs] => { const dispatch = useDispatch(); - const { data, notifications, uiSettings } = useKibana().services; + const { data, notifications } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); - const defaultKibanaIndex = uiSettings.get(DEFAULT_INDEX_KEY); - const defaultIndex = - indexPattern == null || (indexPattern != null && indexPattern.title === '') - ? [ - ...(['all', 'raw'].includes(eventType) ? defaultKibanaIndex : []), - ...(['all', 'alert', 'signal'].includes(eventType) ? indexToAdd : []), - ] - : indexPattern?.title.split(',') ?? []; const [loading, setLoading] = useState(false); const [activePage, setActivePage] = useState(0); - const [timelineRequest, setTimelineRequest] = useState({ - fields, - fieldRequested: fields, - filterQuery: createFilter(filterQuery), - id, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - pagination: { - activePage, - querySize: limit, - }, - sort, - defaultIndex, - docValueFields: docValueFields ?? [], - factoryQueryType: TimelineEventsQueries.all, - }); + const [timelineRequest, setTimelineRequest] = useState( + !skip + ? { + fields, + fieldRequested: fields, + filterQuery: createFilter(filterQuery), + id, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + pagination: { + activePage, + querySize: limit, + }, + sort, + defaultIndex: indexNames, + docValueFields: docValueFields ?? [], + factoryQueryType: TimelineEventsQueries.all, + } + : null + ); const clearSignalsState = useCallback(() => { if (id != null && detectionsTimelineIds.some((timelineId) => timelineId === id)) { @@ -158,7 +142,11 @@ export const useTimelineEvents = ({ }); const timelineSearch = useCallback( - (request: TimelineEventsAllRequestOptions) => { + (request: TimelineEventsAllRequestOptions | null) => { + if (request == null) { + return; + } + let didCancel = false; const asyncSearch = async () => { abortCtrl.current = new AbortController(); @@ -215,14 +203,19 @@ export const useTimelineEvents = ({ ); useEffect(() => { - if (!canQueryTimeline || skipQueryForDetectionsPage(id, defaultIndex)) { + if (skip || skipQueryForDetectionsPage(id, indexNames) || indexNames.length === 0) { return; } setTimelineRequest((prevRequest) => { const myRequest = { - ...prevRequest, - defaultIndex, + ...(prevRequest ?? { + fields, + fieldRequested: fields, + id, + factoryQueryType: TimelineEventsQueries.all, + }), + defaultIndex: indexNames, docValueFields: docValueFields ?? [], filterQuery: createFilter(filterQuery), pagination: { @@ -237,8 +230,8 @@ export const useTimelineEvents = ({ sort, }; if ( - canQueryTimeline && - !skipQueryForDetectionsPage(id, defaultIndex) && + !skip && + !skipQueryForDetectionsPage(id, indexNames) && !deepEqual(prevRequest, myRequest) ) { return myRequest; @@ -246,16 +239,17 @@ export const useTimelineEvents = ({ return prevRequest; }); }, [ - defaultIndex, + indexNames, docValueFields, endDate, filterQuery, startDate, - canQueryTimeline, id, activePage, limit, sort, + skip, + fields, ]); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts index 5e50a7fb3313e..b85fbc15ce3f3 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/one/index.gql_query.ts @@ -107,6 +107,7 @@ export const oneTimelineQuery = gql` serializedQuery } } + indexNames notes { eventId note diff --git a/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts b/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts index c38aa67ccebb2..12d3e6bfd7172 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts +++ b/x-pack/plugins/security_solution/public/timelines/containers/persist.gql_query.ts @@ -95,6 +95,7 @@ export const persistTimelineMutation = gql` serializedQuery } } + indexNames title dateRange { start diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx index f9097ddef6490..3c81aa8dac078 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx @@ -21,12 +21,12 @@ jest.mock('react-router-dom', () => { }; }); jest.mock('../../overview/components/events_by_dataset'); -jest.mock('../../common/containers/source', () => { - const originalModule = jest.requireActual('../../common/containers/source'); +jest.mock('../../common/containers/sourcerer', () => { + const originalModule = jest.requireActual('../../common/containers/sourcerer'); return { ...originalModule, - useWithSource: jest.fn().mockReturnValue({ + useSourcererScope: jest.fn().mockReturnValue({ indicesExist: true, }), }; diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx index 79d0f909c7d59..136240939e7a3 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx @@ -15,16 +15,14 @@ import { WrapperPage } from '../../common/components/wrapper_page'; import { useKibana } from '../../common/lib/kibana'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { useApolloClient } from '../../common/utils/apollo_context'; -import { useWithSource } from '../../common/containers/source'; import { OverviewEmpty } from '../../overview/components/overview_empty'; - import { StatefulOpenTimeline } from '../components/open_timeline'; import { NEW_TEMPLATE_TIMELINE } from '../components/timeline/properties/translations'; import { NewTemplateTimeline } from '../components/timeline/properties/new_template_timeline'; import { NewTimeline } from '../components/timeline/properties/helpers'; - import * as i18n from './translations'; import { SecurityPageName } from '../../app/types'; +import { useSourcererScope } from '../../common/containers/sourcerer'; const TimelinesContainer = styled.div` width: 100%; @@ -38,7 +36,7 @@ export const TimelinesPageComponent: React.FC = () => { const onImportTimelineBtnClick = useCallback(() => { setImportDataModalToggle(true); }, [setImportDataModalToggle]); - const { indicesExist } = useWithSource(); + const { indicesExist } = useSourcererScope(); const apolloClient = useApolloClient(); const capabilitiesCanUserCRUD: boolean = !!useKibana().services.application.capabilities.siem @@ -49,7 +47,7 @@ export const TimelinesPageComponent: React.FC = () => { {indicesExist ? ( <> - + {capabilitiesCanUserCRUD && ( @@ -97,7 +95,7 @@ export const TimelinesPageComponent: React.FC = () => { ) : ( - + )} diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index da9c363703d16..472e82426468e 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -15,9 +15,13 @@ import { } from '../../../timelines/components/timeline/data_providers/data_provider'; import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/types'; -import { EventType, KqlMode, TimelineModel, ColumnHeaderOptions } from './model'; +import { KqlMode, TimelineModel, ColumnHeaderOptions } from './model'; import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; -import { TimelineTypeLiteral, RowRendererId } from '../../../../common/types/timeline'; +import { + TimelineEventsType, + TimelineTypeLiteral, + RowRendererId, +} from '../../../../common/types/timeline'; import { InsertTimeline } from './types'; const actionCreator = actionCreatorFactory('x-pack/security_solution/local/timeline'); @@ -63,6 +67,7 @@ export const createTimeline = actionCreator<{ filters?: Filter[]; columns: ColumnHeaderOptions[]; itemsPerPage?: number; + indexNames: string[]; kqlQuery?: { filterQuery: SerializedFilterQuery | null; filterQueryDraft: KueryFilterQuery | null; @@ -264,7 +269,7 @@ export const clearEventsDeleted = actionCreator<{ id: string; }>('CLEAR_TIMELINE_EVENTS_DELETED'); -export const updateEventType = actionCreator<{ id: string; eventType: EventType }>( +export const updateEventType = actionCreator<{ id: string; eventType: TimelineEventsType }>( 'UPDATE_EVENT_TYPE' ); @@ -272,3 +277,8 @@ export const setExcludedRowRendererIds = actionCreator<{ id: string; excludedRowRendererIds: RowRendererId[]; }>('SET_TIMELINE_EXCLUDED_ROW_RENDERER_IDS'); + +export const updateIndexNames = actionCreator<{ + id: string; + indexNames: string[]; +}>('UPDATE_INDEXES_NAME'); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts index 7980f62cff171..ce469c2bf57a2 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/defaults.ts @@ -27,6 +27,7 @@ export const timelineDefaults: SubsetTimelineModel & Pick { exists: { field: '@timestamp' }, } as Filter, ], + indexNames: [], isFavorite: false, isLive: false, isSelectAllChecked: false, @@ -272,6 +273,7 @@ describe('Epic Timeline', () => { script: null, }, ], + indexNames: [], kqlMode: 'filter', kqlQuery: { filterQuery: { diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts index ad849c3a995b3..cc8e856de1b16 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts @@ -66,6 +66,7 @@ import { updateRange, updateSort, upsertColumn, + updateIndexNames, updateTimeline, updateTitle, updateAutoSaveMsg, @@ -105,6 +106,7 @@ const timelineActionsType = [ updateDescription.type, updateEventType.type, updateKqlMode.type, + updateIndexNames.type, updateProviders.type, updateSort.type, updateTitle.type, @@ -339,6 +341,7 @@ const timelineInput: TimelineInput = { filters: null, kqlMode: null, kqlQuery: null, + indexNames: null, title: null, timelineType: TimelineType.default, templateTimelineVersion: null, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index 8c3f30c75c35b..6507603d30444 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -84,18 +84,16 @@ describe('epicLocalStorage', () => { dataProviders: mockDataProviders, docValueFields: [], end: endDate, - eventType: 'raw' as TimelineComponentProps['eventType'], filters: [], + indexNames: [], indexPattern, - indexToAdd: [], isLive: false, - isLoadingSource: false, isSaving: false, itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], kqlMode: 'search' as TimelineComponentProps['kqlMode'], kqlQueryExpression: '', - loadingIndexName: false, + loadingSourcerer: false, onChangeItemsPerPage: jest.fn(), onClose: jest.fn(), onDataProviderEdited: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts index 1432e133244d0..fc178df86362b 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/helpers.ts @@ -22,6 +22,7 @@ import { import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/model'; import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; import { + TimelineEventsType, TimelineTypeLiteral, TimelineType, RowRendererId, @@ -29,7 +30,7 @@ import { import { normalizeTimeRange } from '../../../common/components/url_state/normalize_time_range'; import { timelineDefaults } from './defaults'; -import { ColumnHeaderOptions, KqlMode, TimelineModel, EventType } from './model'; +import { ColumnHeaderOptions, KqlMode, TimelineModel } from './model'; import { TimelineById } from './types'; export const isNotNull = (value: T | null): value is T => value !== null; @@ -139,6 +140,7 @@ interface AddNewTimelineParams { filters?: Filter[]; id: string; itemsPerPage?: number; + indexNames: string[]; kqlQuery?: { filterQuery: SerializedFilterQuery | null; filterQueryDraft: KueryFilterQuery | null; @@ -159,6 +161,7 @@ export const addNewTimeline = ({ filters = timelineDefaults.filters, id, itemsPerPage = timelineDefaults.itemsPerPage, + indexNames, kqlQuery = { filterQuery: null, filterQueryDraft: null }, sort = timelineDefaults.sort, show = false, @@ -186,6 +189,7 @@ export const addNewTimeline = ({ excludedRowRendererIds, filters, itemsPerPage, + indexNames, kqlQuery, sort, show, @@ -667,7 +671,7 @@ export const updateTimelineTitle = ({ interface UpdateTimelineEventTypeParams { id: string; - eventType: EventType; + eventType: TimelineEventsType; timelineById: TimelineById; } diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index 88ec9da1e0c4e..ec4d37d3b70a2 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -12,6 +12,7 @@ import { PinnedEvent } from '../../../graphql/types'; import { TimelineNonEcsData } from '../../../../common/search_strategy/timeline'; import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/types'; import type { + TimelineEventsType, TimelineType, TimelineStatus, RowRendererId, @@ -19,7 +20,6 @@ import type { export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages export type KqlMode = 'filter' | 'search'; -export type EventType = 'all' | 'raw' | 'alert' | 'signal'; export type ColumnHeaderType = 'not-filtered' | 'text-filter'; @@ -52,7 +52,7 @@ export interface TimelineModel { /** A summary of the events and notes in this timeline */ description: string; /** Typoe of event you want to see in this timeline */ - eventType?: EventType; + eventType?: TimelineEventsType; /** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */ eventIdToNoteIds: Record; /** A list of Ids of excluded Row Renderers */ @@ -66,6 +66,8 @@ export interface TimelineModel { highlightedDropAndProviderId: string; /** Uniquely identifies the timeline */ id: string; + /** TO DO sourcerer @X define this */ + indexNames: string[]; /** If selectAll checkbox in header is checked **/ isSelectAllChecked: boolean; /** Events to be rendered as loading **/ @@ -136,6 +138,7 @@ export type SubsetTimelineModel = Readonly< | 'graphEventId' | 'highlightedDropAndProviderId' | 'historyIds' + | 'indexNames' | 'isFavorite' | 'isLive' | 'isSelectAllChecked' diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index 45bdbd0979276..c2f43625ab464 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from '@elastic/safer-lodash-set/fp'; import { cloneDeep } from 'lodash/fp'; - import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; import { @@ -45,77 +43,76 @@ import { updateTimelineTitle, upsertTimelineColumn, } from './helpers'; -import { ColumnHeaderOptions } from './model'; +import { ColumnHeaderOptions, TimelineModel } from './model'; import { timelineDefaults } from './defaults'; import { TimelineById } from './types'; jest.mock('../../../common/components/url_state/normalize_time_range.ts'); -const timelineByIdMock: TimelineById = { - foo: { - dataProviders: [ - { - and: [], - id: '123', - name: 'data provider 1', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }, - ], - columns: [], - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - excludedRowRendererIds: [], - highlightedDropAndProviderId: '', - historyIds: [], - id: 'foo', - savedObjectId: null, - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50], - kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - noteIds: [], - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - dateRange: { - start: '2020-07-07T08:20:18.966Z', - end: '2020-07-08T08:20:18.966Z', - }, - selectedEventIds: {}, - show: true, - showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, - status: TimelineStatus.active, - width: DEFAULT_TIMELINE_WIDTH, - isSaving: false, - version: null, +const basicDataProvider: DataProvider = { + and: [], + id: '123', + name: 'data provider 1', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, }, + excluded: false, + kqlQuery: '', +}; +const basicTimeline: TimelineModel = { + columns: [], + dataProviders: [{ ...basicDataProvider }], + dateRange: { + start: '2020-07-07T08:20:18.966Z', + end: '2020-07-08T08:20:18.966Z', + }, + deletedEventIds: [], + description: '', + eventIdToNoteIds: {}, + excludedRowRendererIds: [], + highlightedDropAndProviderId: '', + historyIds: [], + id: 'foo', + indexNames: [], + isFavorite: false, + isLive: false, + isLoading: false, + isSaving: false, + isSelectAllChecked: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50], + kqlMode: 'filter', + kqlQuery: { filterQuery: null, filterQueryDraft: null }, + loadingEventIds: [], + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + savedObjectId: null, + selectedEventIds: {}, + show: true, + showCheckboxes: false, + sort: { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + status: TimelineStatus.active, + templateTimelineId: null, + templateTimelineVersion: null, + timelineType: TimelineType.default, + title: '', + version: null, + width: DEFAULT_TIMELINE_WIDTH, +}; +const timelineByIdMock: TimelineById = { + foo: { ...basicTimeline }, }; const timelineByIdTemplateMock: TimelineById = { - ...timelineByIdMock, foo: { - ...timelineByIdMock.foo, + ...basicTimeline, timelineType: TimelineType.template, }, }; @@ -132,14 +129,14 @@ describe('Timeline', () => { const update = addTimelineToStore({ id: 'foo', timeline: { - ...timelineByIdMock.foo, + ...basicTimeline, }, timelineById: timelineByIdMock, }); expect(update).toEqual({ foo: { - ...timelineByIdMock.foo, + ...basicTimeline, show: true, }, }); @@ -151,6 +148,7 @@ describe('Timeline', () => { const update = addNewTimeline({ id: 'bar', columns: defaultHeaders, + indexNames: [], timelineById: timelineByIdMock, timelineType: TimelineType.default, }); @@ -161,27 +159,29 @@ describe('Timeline', () => { const update = addNewTimeline({ id: 'bar', columns: timelineDefaults.columns, + indexNames: [], timelineById: timelineByIdMock, timelineType: TimelineType.default, }); expect(update).toEqual({ - foo: timelineByIdMock.foo, - bar: set('id', 'bar', timelineDefaults), + foo: basicTimeline, + bar: { ...timelineDefaults, id: 'bar' }, }); }); test('should add the specified columns to the timeline', () => { - const barWithEmptyColumns = set('id', 'bar', timelineDefaults); - const barWithPopulatedColumns = set('columns', defaultHeaders, barWithEmptyColumns); + const barWithEmptyColumns = { ...timelineDefaults, id: 'bar' }; + const barWithPopulatedColumns = { ...barWithEmptyColumns, columns: defaultHeaders }; const update = addNewTimeline({ id: 'bar', columns: defaultHeaders, + indexNames: [], timelineById: timelineByIdMock, timelineType: TimelineType.default, }); expect(update).toEqual({ - foo: timelineByIdMock.foo, + foo: basicTimeline, bar: barWithPopulatedColumns, }); }); @@ -203,7 +203,14 @@ describe('Timeline', () => { show: false, // value we are changing from true to false timelineById: timelineByIdMock, }); - expect(update).toEqual(set('foo.show', false, timelineByIdMock)); + + expect(update).toEqual({ + ...timelineByIdMock, + foo: { + ...timelineByIdMock.foo, + show: false, + }, + }); }); }); @@ -211,6 +218,7 @@ describe('Timeline', () => { let timelineById: TimelineById = {}; let columns: ColumnHeaderOptions[] = []; let columnToAdd: ColumnHeaderOptions; + let mockWithExistingColumns: TimelineById; beforeEach(() => { timelineById = cloneDeep(timelineByIdMock); @@ -226,6 +234,13 @@ describe('Timeline', () => { aggregatable: true, width: DEFAULT_COLUMN_MIN_WIDTH, }; + mockWithExistingColumns = { + ...timelineById, + foo: { + ...timelineById.foo, + columns, + }, + }; }); test('should return a new reference and not the same reference', () => { @@ -248,12 +263,11 @@ describe('Timeline', () => { timelineById, }); - expect(update).toEqual(set('foo.columns', expectedColumns, timelineById)); + expect(update.foo.columns).toEqual(expectedColumns); }); test('should add a new column to an existing collection of columns at the beginning of the collection', () => { const expectedColumns = [columnToAdd, ...columns]; - const mockWithExistingColumns = set('foo.columns', columns, timelineById); const update = upsertTimelineColumn({ column: columnToAdd, @@ -261,13 +275,11 @@ describe('Timeline', () => { index: 0, timelineById: mockWithExistingColumns, }); - - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + expect(update.foo.columns).toEqual(expectedColumns); }); test('should add a new column to an existing collection of columns in the middle of the collection', () => { const expectedColumns = [columns[0], columnToAdd, columns[1], columns[2]]; - const mockWithExistingColumns = set('foo.columns', columns, timelineById); const update = upsertTimelineColumn({ column: columnToAdd, @@ -276,12 +288,11 @@ describe('Timeline', () => { timelineById: mockWithExistingColumns, }); - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + expect(update.foo.columns).toEqual(expectedColumns); }); test('should add a new column to an existing collection of columns at the end of the collection', () => { const expectedColumns = [...columns, columnToAdd]; - const mockWithExistingColumns = set('foo.columns', columns, timelineById); const update = upsertTimelineColumn({ column: columnToAdd, @@ -290,13 +301,11 @@ describe('Timeline', () => { timelineById: mockWithExistingColumns, }); - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + expect(update.foo.columns).toEqual(expectedColumns); }); columns.forEach((column, i) => { test(`should upsert (NOT add a new column) a column when already exists at the same index (${i})`, () => { - const mockWithExistingColumns = set('foo.columns', columns, timelineById); - const update = upsertTimelineColumn({ column, id: 'foo', @@ -304,13 +313,12 @@ describe('Timeline', () => { timelineById: mockWithExistingColumns, }); - expect(update).toEqual(set('foo.columns', columns, mockWithExistingColumns)); + expect(update.foo.columns).toEqual(columns); }); }); test('should allow the 1st column to be moved to the 2nd column', () => { const expectedColumns = [columns[1], columns[0], columns[2]]; - const mockWithExistingColumns = set('foo.columns', columns, timelineById); const update = upsertTimelineColumn({ column: columns[0], @@ -319,12 +327,11 @@ describe('Timeline', () => { timelineById: mockWithExistingColumns, }); - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + expect(update.foo.columns).toEqual(expectedColumns); }); test('should allow the 1st column to be moved to the 3rd column', () => { const expectedColumns = [columns[1], columns[2], columns[0]]; - const mockWithExistingColumns = set('foo.columns', columns, timelineById); const update = upsertTimelineColumn({ column: columns[0], @@ -333,12 +340,11 @@ describe('Timeline', () => { timelineById: mockWithExistingColumns, }); - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + expect(update.foo.columns).toEqual(expectedColumns); }); test('should allow the 2nd column to be moved to the 1st column', () => { const expectedColumns = [columns[1], columns[0], columns[2]]; - const mockWithExistingColumns = set('foo.columns', columns, timelineById); const update = upsertTimelineColumn({ column: columns[1], @@ -347,12 +353,11 @@ describe('Timeline', () => { timelineById: mockWithExistingColumns, }); - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + expect(update.foo.columns).toEqual(expectedColumns); }); test('should allow the 2nd column to be moved to the 3rd column', () => { const expectedColumns = [columns[0], columns[2], columns[1]]; - const mockWithExistingColumns = set('foo.columns', columns, timelineById); const update = upsertTimelineColumn({ column: columns[1], @@ -361,12 +366,11 @@ describe('Timeline', () => { timelineById: mockWithExistingColumns, }); - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + expect(update.foo.columns).toEqual(expectedColumns); }); test('should allow the 3rd column to be moved to the 1st column', () => { const expectedColumns = [columns[2], columns[0], columns[1]]; - const mockWithExistingColumns = set('foo.columns', columns, timelineById); const update = upsertTimelineColumn({ column: columns[2], @@ -375,12 +379,11 @@ describe('Timeline', () => { timelineById: mockWithExistingColumns, }); - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + expect(update.foo.columns).toEqual(expectedColumns); }); test('should allow the 3rd column to be moved to the 2nd column', () => { const expectedColumns = [columns[0], columns[2], columns[1]]; - const mockWithExistingColumns = set('foo.columns', columns, timelineById); const update = upsertTimelineColumn({ column: columns[2], @@ -389,75 +392,39 @@ describe('Timeline', () => { timelineById: mockWithExistingColumns, }); - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + expect(update.foo.columns).toEqual(expectedColumns); }); }); describe('#addTimelineProvider', () => { + const providerToAdd: DataProvider = { + ...basicDataProvider, + id: '567', + name: 'data provider 2', + }; test('should return a new reference and not the same reference', () => { const update = addTimelineProvider({ id: 'foo', - provider: { - and: [], - id: '567', - name: 'data provider 2', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }, + provider: providerToAdd, timelineById: timelineByIdMock, }); expect(update).not.toBe(timelineByIdMock); }); test('should add a new timeline provider', () => { - const providerToAdd: DataProvider = { - and: [], - id: '567', - name: 'data provider 2', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }; const update = addTimelineProvider({ id: 'foo', provider: providerToAdd, timelineById: timelineByIdMock, }); - const addedDataProvider = timelineByIdMock.foo.dataProviders.concat(providerToAdd); - expect(update).toEqual(set('foo.dataProviders', addedDataProvider, timelineByIdMock)); + const addedDataProvider = [...basicTimeline.dataProviders].concat(providerToAdd); + expect(update.foo.dataProviders).toEqual(addedDataProvider); }); test('should NOT add a new timeline provider if it already exists and the attributes "and" is empty', () => { - const providerToAdd: DataProvider = { - and: [], - id: '123', - name: 'data provider 1', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }; const update = addTimelineProvider({ id: 'foo', - provider: providerToAdd, + provider: basicDataProvider, timelineById: timelineByIdMock, }); expect(update).toEqual(timelineByIdMock); @@ -467,68 +434,45 @@ describe('Timeline', () => { const myMockTimelineByIdMock = cloneDeep(timelineByIdMock); myMockTimelineByIdMock.foo.dataProviders[0].and = [ { + ...basicDataProvider, id: '456', name: 'and data provider 1', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, }, ]; - const providerToAdd: DataProvider = { - and: [], - id: '123', - name: 'data provider 1', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }; + const provider = { ...basicDataProvider }; const update = addTimelineProvider({ id: 'foo', - provider: providerToAdd, + provider, timelineById: myMockTimelineByIdMock, }); - expect(update).toEqual(set('foo.dataProviders[1]', providerToAdd, myMockTimelineByIdMock)); + expect(update.foo.dataProviders[1]).toEqual(provider); }); test('should UPSERT an existing timeline provider if it already exists', () => { - const providerToAdd: DataProvider = { - and: [], - id: '123', - name: 'my name changed', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - excluded: false, - kqlQuery: '', - }; const update = addTimelineProvider({ id: 'foo', - provider: providerToAdd, + provider: { + ...basicDataProvider, + name: 'my name changed', + }, timelineById: timelineByIdMock, }); - expect(update).toEqual(set('foo.dataProviders[0].name', 'my name changed', timelineByIdMock)); + expect(update.foo.dataProviders[0].name).toEqual('my name changed'); }); }); describe('#removeTimelineColumn', () => { + let mockWithExistingColumns: TimelineById; + beforeEach(() => { + mockWithExistingColumns = { + ...timelineByIdMock, + foo: { + ...timelineByIdMock.foo, + columns: columnsMock, + }, + }; + }); test('should return a new reference and not the same reference', () => { - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - const update = removeTimelineColumn({ id: 'foo', columnId: columnsMock[0].id, @@ -541,70 +485,65 @@ describe('Timeline', () => { test('should remove just the first column when the id matches', () => { const expectedColumns = [columnsMock[1], columnsMock[2]]; - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - const update = removeTimelineColumn({ id: 'foo', columnId: columnsMock[0].id, timelineById: mockWithExistingColumns, }); - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + expect(update.foo.columns).toEqual(expectedColumns); }); test('should remove just the last column when the id matches', () => { const expectedColumns = [columnsMock[0], columnsMock[1]]; - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - const update = removeTimelineColumn({ id: 'foo', columnId: columnsMock[2].id, timelineById: mockWithExistingColumns, }); - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + expect(update.foo.columns).toEqual(expectedColumns); }); test('should remove just the middle column when the id matches', () => { const expectedColumns = [columnsMock[0], columnsMock[2]]; - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - const update = removeTimelineColumn({ id: 'foo', columnId: columnsMock[1].id, timelineById: mockWithExistingColumns, }); - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + expect(update.foo.columns).toEqual(expectedColumns); }); test('should not modify the columns if the id to remove was not found', () => { const expectedColumns = cloneDeep(columnsMock); - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - const update = removeTimelineColumn({ id: 'foo', columnId: 'does.not.exist', timelineById: mockWithExistingColumns, }); - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + expect(update.foo.columns).toEqual(expectedColumns); }); }); describe('#applyDeltaToColumnWidth', () => { + let mockWithExistingColumns: TimelineById; + beforeEach(() => { + mockWithExistingColumns = { + ...timelineByIdMock, + foo: { + ...timelineByIdMock.foo, + columns: columnsMock, + }, + }; + }); test('should return a new reference and not the same reference', () => { const delta = 50; - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - const update = applyDeltaToTimelineColumnWidth({ id: 'foo', columnId: columnsMock[0].id, @@ -624,9 +563,6 @@ describe('Timeline', () => { }; const expectedColumns = [expectedToHaveNewWidth, columnsMock[1], columnsMock[2]]; - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - const update = applyDeltaToTimelineColumnWidth({ id: 'foo', columnId: aDateColumn.id, @@ -634,7 +570,7 @@ describe('Timeline', () => { timelineById: mockWithExistingColumns, }); - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + expect(update.foo.columns).toEqual(expectedColumns); }); test('should NOT update (just) the specified column of type `date` when the id matches, because the result of applying the delta is less than the min width for a date column', () => { @@ -646,9 +582,6 @@ describe('Timeline', () => { }; const expectedColumns = [expectedToHaveNewWidth, columnsMock[1], columnsMock[2]]; - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - const update = applyDeltaToTimelineColumnWidth({ id: 'foo', columnId: aDateColumn.id, @@ -656,7 +589,7 @@ describe('Timeline', () => { timelineById: mockWithExistingColumns, }); - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + expect(update.foo.columns).toEqual(expectedColumns); }); test('should update (just) the specified non-date column when the id matches, and the result of applying the delta is greater than the min width for the column', () => { @@ -668,9 +601,6 @@ describe('Timeline', () => { }; const expectedColumns = [columnsMock[0], expectedToHaveNewWidth, columnsMock[2]]; - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - const update = applyDeltaToTimelineColumnWidth({ id: 'foo', columnId: aNonDateColumn.id, @@ -678,7 +608,7 @@ describe('Timeline', () => { timelineById: mockWithExistingColumns, }); - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + expect(update.foo.columns).toEqual(expectedColumns); }); test('should NOT update the specified non-date column when the id matches, because the result of applying the delta is less than the min width for the column', () => { @@ -690,9 +620,6 @@ describe('Timeline', () => { }; const expectedColumns = [columnsMock[0], expectedToHaveNewWidth, columnsMock[2]]; - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - const update = applyDeltaToTimelineColumnWidth({ id: 'foo', columnId: aNonDateColumn.id, @@ -700,24 +627,21 @@ describe('Timeline', () => { timelineById: mockWithExistingColumns, }); - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + expect(update.foo.columns).toEqual(expectedColumns); }); }); describe('#addAndProviderToTimelineProvider', () => { test('should add a new and provider to an existing timeline provider', () => { const providerToAdd: DataProvider = { - and: [], + ...basicDataProvider, id: '567', name: 'data provider 2', - enabled: true, queryMatch: { field: 'handsome', - value: 'garrett', + value: 'xavier', operator: IS_OPERATOR, }, - excluded: false, - kqlQuery: '', }; const newTimeline = addTimelineProvider({ @@ -729,18 +653,14 @@ describe('Timeline', () => { newTimeline.foo.highlightedDropAndProviderId = '567'; const andProviderToAdd: DataProvider = { - and: [], + ...basicDataProvider, id: '568', name: 'And Data Provider', - enabled: true, queryMatch: { field: 'smart', - value: 'frank', + value: 'steph', operator: IS_OPERATOR, }, - - excluded: false, - kqlQuery: '', }; const update = addTimelineProvider({ @@ -757,30 +677,25 @@ describe('Timeline', () => { test('should add another and provider because it is not a duplicate', () => { const providerToAdd: DataProvider = { + ...basicDataProvider, and: [ { + ...basicDataProvider, id: '568', name: 'And Data Provider', - enabled: true, queryMatch: { field: 'smart', - value: 'garrett', + value: 'xavier', operator: IS_OPERATOR, }, - excluded: false, - kqlQuery: '', }, ], id: '567', - name: 'data provider 1', - enabled: true, queryMatch: { field: 'handsome', - value: 'frank', + value: 'steph', operator: IS_OPERATOR, }, - excluded: false, - kqlQuery: '', }; const newTimeline = addTimelineProvider({ @@ -792,17 +707,14 @@ describe('Timeline', () => { newTimeline.foo.highlightedDropAndProviderId = '567'; const andProviderToAdd: DataProvider = { - and: [], + ...basicDataProvider, id: '569', name: 'And Data Provider', - enabled: true, queryMatch: { field: 'happy', value: 'andrewG', operator: IS_OPERATOR, }, - excluded: false, - kqlQuery: '', }; // temporary, we will have to decouple DataProvider & DataProvidersAnd // that's bigger a refactor than just fixing a bug @@ -814,36 +726,31 @@ describe('Timeline', () => { timelineById: newTimeline, }); - expect(update).toEqual(set('foo.dataProviders[1].and[1]', andProviderToAdd, newTimeline)); + expect(update.foo.dataProviders[1].and[1]).toEqual(andProviderToAdd); newTimeline.foo.highlightedDropAndProviderId = ''; }); test('should NOT add another and provider because it is a duplicate', () => { const providerToAdd: DataProvider = { + ...basicDataProvider, and: [ { + ...basicDataProvider, id: '568', name: 'And Data Provider', - enabled: true, queryMatch: { field: 'smart', - value: 'garrett', + value: 'xavier', operator: IS_OPERATOR, }, - excluded: false, - kqlQuery: '', }, ], id: '567', - name: 'data provider 1', - enabled: true, queryMatch: { field: 'handsome', - value: 'frank', + value: 'steph', operator: IS_OPERATOR, }, - excluded: false, - kqlQuery: '', }; const newTimeline = addTimelineProvider({ @@ -855,17 +762,14 @@ describe('Timeline', () => { newTimeline.foo.highlightedDropAndProviderId = '567'; const andProviderToAdd: DataProvider = { - and: [], + ...basicDataProvider, id: '569', name: 'And Data Provider', - enabled: true, queryMatch: { field: 'smart', - value: 'garrett', + value: 'xavier', operator: IS_OPERATOR, }, - excluded: false, - kqlQuery: '', }; const update = addTimelineProvider({ id: 'foo', @@ -894,7 +798,7 @@ describe('Timeline', () => { columns: columnsMock, timelineById: timelineByIdMock, }); - expect(update).toEqual(set('foo.columns', [...columnsMock], timelineByIdMock)); + expect(update.foo.columns).toEqual([...columnsMock]); }); }); @@ -916,7 +820,7 @@ describe('Timeline', () => { description: newDescription, timelineById: timelineByIdMock, }); - expect(update).toEqual(set('foo.description', newDescription, timelineByIdMock)); + expect(update.foo.description).toEqual(newDescription); }); test('should always trim all leading whitespace and allow only one trailing space', () => { @@ -925,7 +829,7 @@ describe('Timeline', () => { description: ' breathing room ', timelineById: timelineByIdMock, }); - expect(update).toEqual(set('foo.description', 'breathing room ', timelineByIdMock)); + expect(update.foo.description).toEqual('breathing room '); }); }); @@ -947,7 +851,7 @@ describe('Timeline', () => { title: newTitle, timelineById: timelineByIdMock, }); - expect(update).toEqual(set('foo.title', newTitle, timelineByIdMock)); + expect(update.foo.title).toEqual(newTitle); }); test('should always trim all leading whitespace and allow only one trailing space', () => { @@ -956,7 +860,7 @@ describe('Timeline', () => { title: ' room at the back ', timelineById: timelineByIdMock, }); - expect(update).toEqual(set('foo.title', 'room at the back ', timelineByIdMock)); + expect(update.foo.title).toEqual('room at the back '); }); }); @@ -966,18 +870,9 @@ describe('Timeline', () => { id: 'foo', providers: [ { - and: [], + ...basicDataProvider, id: '567', name: 'data provider 2', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', }, ], timelineById: timelineByIdMock, @@ -985,64 +880,47 @@ describe('Timeline', () => { expect(update).not.toBe(timelineByIdMock); }); - test('should add update a timeline with new providers', () => { + test('should add update a timeline with new providers BBB', () => { const providerToAdd: DataProvider = { - and: [], + ...basicDataProvider, id: '567', name: 'data provider 2', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', }; const update = updateTimelineProviders({ id: 'foo', providers: [providerToAdd], timelineById: timelineByIdMock, }); - expect(update).toEqual(set('foo.dataProviders', [providerToAdd], timelineByIdMock)); + expect(update.foo.dataProviders).toEqual([providerToAdd]); }); }); describe('#updateTimelineRange', () => { - test('should return a new reference and not the same reference', () => { - const update = updateTimelineRange({ + let update: TimelineById; + beforeAll(() => { + update = updateTimelineRange({ id: 'foo', start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z', timelineById: timelineByIdMock, }); + }); + test('should return a new reference and not the same reference', () => { expect(update).not.toBe(timelineByIdMock); }); test('should update the timeline range', () => { - const update = updateTimelineRange({ - id: 'foo', + expect(update.foo.dateRange).toEqual({ start: '2020-07-07T08:20:18.966Z', end: '2020-07-08T08:20:18.966Z', - timelineById: timelineByIdMock, }); - expect(update).toEqual( - set( - 'foo.dateRange', - { - start: '2020-07-07T08:20:18.966Z', - end: '2020-07-08T08:20:18.966Z', - }, - timelineByIdMock - ) - ); }); }); describe('#updateTimelineSort', () => { - test('should return a new reference and not the same reference', () => { - const update = updateTimelineSort({ + let update: TimelineById; + beforeAll(() => { + update = updateTimelineSort({ id: 'foo', sort: { columnId: 'some column', @@ -1050,31 +928,19 @@ describe('Timeline', () => { }, timelineById: timelineByIdMock, }); + }); + test('should return a new reference and not the same reference', () => { expect(update).not.toBe(timelineByIdMock); }); test('should update the timeline range', () => { - const update = updateTimelineSort({ - id: 'foo', - sort: { - columnId: 'some column', - sortDirection: Direction.desc, - }, - timelineById: timelineByIdMock, - }); - expect(update).toEqual( - set( - 'foo.sort', - { columnId: 'some column', sortDirection: Direction.desc }, - timelineByIdMock - ) - ); + expect(update.foo.sort).toEqual({ columnId: 'some column', sortDirection: Direction.desc }); }); }); describe('#updateTimelineProviderEnabled', () => { test('should return a new reference and not the same reference', () => { - const update = updateTimelineProviderEnabled({ + const update: TimelineById = updateTimelineProviderEnabled({ id: 'foo', providerId: '123', enabled: false, // value we are updating from true to false @@ -1084,17 +950,17 @@ describe('Timeline', () => { }); test('should return a new reference for data provider and not the same reference of data provider', () => { - const update = updateTimelineProviderEnabled({ + const update: TimelineById = updateTimelineProviderEnabled({ id: 'foo', providerId: '123', enabled: false, // value we are updating from true to false timelineById: timelineByIdMock, }); - expect(update.foo.dataProviders).not.toBe(timelineByIdMock.foo.dataProviders); + expect(update.foo.dataProviders).not.toBe(basicTimeline.dataProviders); }); test('should update the timeline provider enabled from true to false', () => { - const update = updateTimelineProviderEnabled({ + const update: TimelineById = updateTimelineProviderEnabled({ id: 'foo', providerId: '123', enabled: false, // value we are updating from true to false @@ -1102,81 +968,29 @@ describe('Timeline', () => { }); const expected: TimelineById = { foo: { - id: 'foo', - savedObjectId: null, - columns: [], + ...basicTimeline, dataProviders: [ { - and: [], - id: '123', - name: 'data provider 1', - enabled: false, // This value changed from true to false - excluded: false, - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, + ...basicDataProvider, + enabled: false, }, ], - deletedEventIds: [], - description: '', - eventIdToNoteIds: {}, - excludedRowRendererIds: [], - highlightedDropAndProviderId: '', - historyIds: [], - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - noteIds: [], - dateRange: { - start: '2020-07-07T08:20:18.966Z', - end: '2020-07-08T08:20:18.966Z', - }, - selectedEventIds: {}, - show: true, - showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, - status: TimelineStatus.active, - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50], - width: DEFAULT_TIMELINE_WIDTH, - isSaving: false, - version: null, }, }; expect(update).toEqual(expected); }); test('should update only one data provider and not two data providers', () => { - const multiDataProvider = timelineByIdMock.foo.dataProviders.concat({ - and: [], + const multiDataProvider = [...basicTimeline.dataProviders].concat({ + ...basicDataProvider, id: '456', - name: 'data provider 1', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, }); - const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock); + const multiDataProviderMock = { + foo: { + ...timelineByIdMock.foo, + dataProviders: multiDataProvider, + }, + }; const update = updateTimelineProviderEnabled({ id: 'foo', providerId: '123', @@ -1185,74 +999,17 @@ describe('Timeline', () => { }); const expected: TimelineById = { foo: { - id: 'foo', - savedObjectId: null, - columns: [], + ...basicTimeline, dataProviders: [ { - and: [], - id: '123', - name: 'data provider 1', - enabled: false, // value we are updating from true to false - excluded: false, - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, + ...basicDataProvider, + enabled: false, }, { - and: [], + ...basicDataProvider, id: '456', - name: 'data provider 1', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, }, ], - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - excludedRowRendererIds: [], - highlightedDropAndProviderId: '', - historyIds: [], - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - noteIds: [], - dateRange: { - start: '2020-07-07T08:20:18.966Z', - end: '2020-07-08T08:20:18.966Z', - }, - selectedEventIds: {}, - show: true, - showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, - status: TimelineStatus.active, - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50], - width: DEFAULT_TIMELINE_WIDTH, - isSaving: false, - version: null, }, }; expect(update).toEqual(expected); @@ -1261,34 +1018,18 @@ describe('Timeline', () => { describe('#updateTimelineAndProviderEnabled', () => { let timelineByIdwithAndMock: TimelineById = timelineByIdMock; + let update: TimelineById; beforeEach(() => { const providerToAdd: DataProvider = { + ...basicDataProvider, and: [ { + ...basicDataProvider, id: '568', name: 'And Data Provider', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', }, ], id: '567', - name: 'data provider 1', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', }; timelineByIdwithAndMock = addTimelineProvider({ @@ -1296,43 +1037,30 @@ describe('Timeline', () => { provider: providerToAdd, timelineById: timelineByIdMock, }); - }); - test('should return a new reference and not the same reference', () => { - const update = updateTimelineProviderEnabled({ + update = updateTimelineProviderEnabled({ id: 'foo', providerId: '567', enabled: false, // value we are updating from true to false timelineById: timelineByIdwithAndMock, andProviderId: '568', }); + }); + + test('should return a new reference and not the same reference', () => { expect(update).not.toBe(timelineByIdwithAndMock); }); test('should return a new reference for and data provider and not the same reference of data and provider', () => { - const update = updateTimelineProviderEnabled({ - id: 'foo', - providerId: '567', - enabled: false, // value we are updating from true to false - timelineById: timelineByIdwithAndMock, - andProviderId: '568', - }); - expect(update.foo.dataProviders).not.toBe(timelineByIdMock.foo.dataProviders); + expect(update.foo.dataProviders).not.toBe(basicTimeline.dataProviders); }); test('should update the timeline and provider enabled from true to false', () => { - const update = updateTimelineProviderEnabled({ - id: 'foo', - providerId: '567', - enabled: false, // value we are updating from true to false - timelineById: timelineByIdwithAndMock, - andProviderId: '568', - }); const indexProvider = update.foo.dataProviders.findIndex((i) => i.id === '567'); expect(update.foo.dataProviders[indexProvider].and[0].enabled).toEqual(false); }); - test('should update only one and data provider and not two and data providers', () => { + test('should update only one and data provider and not two and data providers ahhhh', () => { const indexProvider = timelineByIdwithAndMock.foo.dataProviders.findIndex( (i) => i.id === '567' ); @@ -1351,12 +1079,9 @@ describe('Timeline', () => { excluded: false, kqlQuery: '', }); - const multiAndDataProviderMock = set( - `foo.dataProviders[${indexProvider}].and`, - multiAndDataProvider, - timelineByIdwithAndMock - ); - const update = updateTimelineProviderEnabled({ + const multiAndDataProviderMock = timelineByIdwithAndMock; + multiAndDataProviderMock.foo.dataProviders[indexProvider].and = multiAndDataProvider; + update = updateTimelineProviderEnabled({ id: 'foo', providerId: '567', enabled: false, // value we are updating from true to false @@ -1375,111 +1100,51 @@ describe('Timeline', () => { }); describe('#updateTimelineProviderExcluded', () => { - test('should return a new reference and not the same reference', () => { - const update = updateTimelineProviderExcluded({ + let update: TimelineById; + beforeAll(() => { + update = updateTimelineProviderExcluded({ id: 'foo', providerId: '123', excluded: true, // value we are updating from false to true timelineById: timelineByIdMock, }); + }); + test('should return a new reference and not the same reference', () => { expect(update).not.toBe(timelineByIdMock); }); test('should return a new reference for data provider and not the same reference of data provider', () => { - const update = updateTimelineProviderExcluded({ - id: 'foo', - providerId: '123', - excluded: true, // value we are updating from false to true - timelineById: timelineByIdMock, - }); - expect(update.foo.dataProviders).not.toBe(timelineByIdMock.foo.dataProviders); + expect(update.foo.dataProviders).not.toBe(basicTimeline.dataProviders); }); test('should update the timeline provider excluded from true to false', () => { - const update = updateTimelineProviderExcluded({ - id: 'foo', - providerId: '123', - excluded: true, // value we are updating from false to true - timelineById: timelineByIdMock, - }); const expected: TimelineById = { foo: { - id: 'foo', - savedObjectId: null, - columns: [], + ...basicTimeline, dataProviders: [ { - and: [], - id: '123', - name: 'data provider 1', - enabled: true, - excluded: true, // This value changed from true to false - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, + ...basicDataProvider, + excluded: true, }, ], - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - excludedRowRendererIds: [], - highlightedDropAndProviderId: '', - historyIds: [], - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - noteIds: [], - dateRange: { - start: '2020-07-07T08:20:18.966Z', - end: '2020-07-08T08:20:18.966Z', - }, - selectedEventIds: {}, - show: true, - showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, - status: TimelineStatus.active, - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50], - width: DEFAULT_TIMELINE_WIDTH, - isSaving: false, - version: null, }, }; expect(update).toEqual(expected); }); test('should update only one data provider and not two data providers', () => { - const multiDataProvider = timelineByIdMock.foo.dataProviders.concat({ - and: [], + const multiDataProvider = basicTimeline.dataProviders.concat({ + ...basicDataProvider, id: '456', - name: 'data provider 1', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, }); - const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock); - const update = updateTimelineProviderExcluded({ + const multiDataProviderMock = { + ...timelineByIdMock, + foo: { + ...timelineByIdMock.foo, + dataProviders: multiDataProvider, + }, + }; + update = updateTimelineProviderExcluded({ id: 'foo', providerId: '123', excluded: true, // value we are updating from false to true @@ -1487,74 +1152,17 @@ describe('Timeline', () => { }); const expected: TimelineById = { foo: { - id: 'foo', - savedObjectId: null, - columns: [], + ...basicTimeline, dataProviders: [ { - and: [], - id: '123', - name: 'data provider 1', - enabled: true, + ...basicDataProvider, excluded: true, // value we are updating from false to true - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, }, { - and: [], + ...basicDataProvider, id: '456', - name: 'data provider 1', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, }, ], - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - excludedRowRendererIds: [], - highlightedDropAndProviderId: '', - historyIds: [], - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.default, - templateTimelineId: null, - templateTimelineVersion: null, - noteIds: [], - dateRange: { - start: '2020-07-07T08:20:18.966Z', - end: '2020-07-08T08:20:18.966Z', - }, - selectedEventIds: {}, - show: true, - showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, - status: TimelineStatus.active, - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50], - width: DEFAULT_TIMELINE_WIDTH, - isSaving: false, - version: null, }, }; expect(update).toEqual(expected); @@ -1596,173 +1204,71 @@ describe('Timeline', () => { const update = updateTimelineProviderType({ id: 'foo', providerId: '123', - type: DataProviderType.template, // value we are updating from default to template + type: DataProviderType.template, timelineById: timelineByIdTemplateMock, }); const expected: TimelineById = { foo: { - id: 'foo', - savedObjectId: null, - columns: [], + ...basicTimeline, dataProviders: [ { - and: [], - id: '123', - name: '', // This value changed - enabled: true, - excluded: false, - kqlQuery: '', - type: DataProviderType.template, // value we are updating from default to template + ...basicDataProvider, + name: '', queryMatch: { field: '', - value: '{}', // This value changed + value: '{}', operator: IS_OPERATOR, }, + type: DataProviderType.template, }, ], - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - excludedRowRendererIds: [], - highlightedDropAndProviderId: '', - historyIds: [], - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, - loadingEventIds: [], - title: '', timelineType: TimelineType.template, - templateTimelineVersion: null, - templateTimelineId: null, - noteIds: [], - dateRange: { - start: '2020-07-07T08:20:18.966Z', - end: '2020-07-08T08:20:18.966Z', - }, - selectedEventIds: {}, - show: true, - showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, - status: TimelineStatus.active, - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50], - width: DEFAULT_TIMELINE_WIDTH, - isSaving: false, - version: null, }, }; + expect(update).toEqual(expected); }); + test('should update only one data provider and not two data providers AHH', () => { + const multiDataProvider = [ + ...timelineByIdTemplateMock.foo.dataProviders, + { + ...basicDataProvider, + id: '456', + type: DataProviderType.template, + }, + ]; - test('should update only one data provider and not two data providers', () => { - const multiDataProvider = timelineByIdTemplateMock.foo.dataProviders.concat({ - and: [], - id: '456', - name: 'data provider 1', - enabled: true, - excluded: false, - type: DataProviderType.template, - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, + const multiDataProviderMock = { + ...timelineByIdTemplateMock, + foo: { + ...timelineByIdTemplateMock.foo, + dataProviders: multiDataProvider, }, - }); - const multiDataProviderMock = set( - 'foo.dataProviders', - multiDataProvider, - timelineByIdTemplateMock - ); + }; const update = updateTimelineProviderType({ id: 'foo', providerId: '123', type: DataProviderType.template, // value we are updating from default to template timelineById: multiDataProviderMock, }); - const expected: TimelineById = { - foo: { - id: 'foo', - savedObjectId: null, - columns: [], - dataProviders: [ - { - and: [], - id: '123', - name: '', - enabled: true, - excluded: false, - type: DataProviderType.template, // value we are updating from default to template - kqlQuery: '', - queryMatch: { - field: '', - value: '{}', - operator: IS_OPERATOR, - }, - }, - { - and: [], - id: '456', - name: 'data provider 1', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - type: DataProviderType.template, - }, - ], - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - excludedRowRendererIds: [], - highlightedDropAndProviderId: '', - historyIds: [], - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.template, - templateTimelineId: null, - templateTimelineVersion: null, - noteIds: [], - dateRange: { - start: '2020-07-07T08:20:18.966Z', - end: '2020-07-08T08:20:18.966Z', - }, - selectedEventIds: {}, - show: true, - showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, + const expected = [ + { + ...basicDataProvider, + name: '', + type: DataProviderType.template, + queryMatch: { + field: '', + value: '{}', + operator: IS_OPERATOR, }, - status: TimelineStatus.active, - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50], - width: DEFAULT_TIMELINE_WIDTH, - isSaving: false, - version: null, }, - }; - expect(update).toEqual(expected); + { + ...basicDataProvider, + id: '456', + type: DataProviderType.template, + }, + ]; + expect(update.foo.dataProviders).toEqual(expected); }); }); @@ -1770,32 +1276,15 @@ describe('Timeline', () => { let timelineByIdwithAndMock: TimelineById = timelineByIdMock; beforeEach(() => { const providerToAdd: DataProvider = { + ...basicDataProvider, and: [ { + ...basicDataProvider, id: '568', name: 'And Data Provider', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', }, ], id: '567', - name: 'data provider 1', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', }; timelineByIdwithAndMock = addTimelineProvider({ @@ -1824,7 +1313,7 @@ describe('Timeline', () => { timelineById: timelineByIdwithAndMock, andProviderId: '568', }); - expect(update.foo.dataProviders).not.toBe(timelineByIdMock.foo.dataProviders); + expect(update.foo.dataProviders).not.toBe(basicTimeline.dataProviders); }); test('should update the timeline and provider excluded from true to false', () => { @@ -1846,23 +1335,12 @@ describe('Timeline', () => { const multiAndDataProvider = timelineByIdwithAndMock.foo.dataProviders[ indexProvider ].and.concat({ + ...basicDataProvider, id: '456', name: 'new and data provider', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', }); - const multiAndDataProviderMock = set( - `foo.dataProviders[${indexProvider}].and`, - multiAndDataProvider, - timelineByIdwithAndMock - ); + const multiAndDataProviderMock = timelineByIdwithAndMock; + multiAndDataProviderMock.foo.dataProviders[indexProvider].and = multiAndDataProvider; const update = updateTimelineProviderExcluded({ id: 'foo', providerId: '567', @@ -1899,62 +1377,8 @@ describe('Timeline', () => { }); const expected: TimelineById = { foo: { - id: 'foo', - savedObjectId: null, - columns: [], - dataProviders: [ - { - and: [], - id: '123', - name: 'data provider 1', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }, - ], - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - excludedRowRendererIds: [], - highlightedDropAndProviderId: '', - historyIds: [], - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - noteIds: [], - dateRange: { - start: '2020-07-07T08:20:18.966Z', - end: '2020-07-08T08:20:18.966Z', - }, - selectedEventIds: {}, - show: true, - showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, - status: TimelineStatus.active, - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, + ...basicTimeline, itemsPerPage: 50, - itemsPerPageOptions: [10, 25, 50], - width: DEFAULT_TIMELINE_WIDTH, - isSaving: false, - version: null, }, }; expect(update).toEqual(expected); @@ -1979,62 +1403,8 @@ describe('Timeline', () => { }); const expected: TimelineById = { foo: { - columns: [], - dataProviders: [ - { - and: [], - id: '123', - name: 'data provider 1', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }, - ], - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - excludedRowRendererIds: [], - highlightedDropAndProviderId: '', - historyIds: [], - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - id: 'foo', - savedObjectId: null, - kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - noteIds: [], - dateRange: { - start: '2020-07-07T08:20:18.966Z', - end: '2020-07-08T08:20:18.966Z', - }, - selectedEventIds: {}, - show: true, - showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, - status: TimelineStatus.active, - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - itemsPerPage: 25, + ...basicTimeline, itemsPerPageOptions: [100, 200, 300], // updated - width: DEFAULT_TIMELINE_WIDTH, - isSaving: false, - version: null, }, }; expect(update).toEqual(expected); @@ -2057,25 +1427,22 @@ describe('Timeline', () => { providerId: '123', timelineById: timelineByIdMock, }); - expect(update).toEqual(set('foo.dataProviders', [], timelineByIdMock)); + expect(update.foo.dataProviders).toEqual([]); }); test('should remove only one data provider and not two data providers', () => { - const multiDataProvider = timelineByIdMock.foo.dataProviders.concat({ - and: [], + const multiDataProvider = basicTimeline.dataProviders.concat({ + ...basicDataProvider, id: '456', name: 'data provider 2', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', }); - const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock); + const multiDataProviderMock = { + ...timelineByIdMock, + foo: { + ...timelineByIdMock.foo, + dataProviders: multiDataProvider, + }, + }; const update = removeTimelineProvider({ id: 'foo', providerId: '123', @@ -2083,62 +1450,14 @@ describe('Timeline', () => { }); const expected: TimelineById = { foo: { - columns: [], + ...basicTimeline, dataProviders: [ { - and: [], + ...basicDataProvider, id: '456', name: 'data provider 2', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', }, ], - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - excludedRowRendererIds: [], - highlightedDropAndProviderId: '', - historyIds: [], - id: 'foo', - savedObjectId: null, - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - noteIds: [], - dateRange: { - start: '2020-07-07T08:20:18.966Z', - end: '2020-07-08T08:20:18.966Z', - }, - selectedEventIds: {}, - show: true, - showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, - status: TimelineStatus.active, - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50], - width: DEFAULT_TIMELINE_WIDTH, - isSaving: false, - version: null, }, }; expect(update).toEqual(expected); @@ -2147,99 +1466,58 @@ describe('Timeline', () => { test('should remove only first provider and not nested andProvider', () => { const dataProviders: DataProvider[] = [ { - and: [], + ...basicDataProvider, id: '111', - name: 'data provider 1', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', }, { - and: [], + ...basicDataProvider, id: '222', name: 'data provider 2', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', }, { - and: [], + ...basicDataProvider, id: '333', name: 'data provider 3', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', }, ]; - const multiDataProviderMock = set('foo.dataProviders', dataProviders, timelineByIdMock); - + const multiDataProviderMock = { + ...timelineByIdMock, + foo: { + ...timelineByIdMock.foo, + dataProviders, + }, + }; const andDataProvider: DataProvidersAnd = { + ...basicDataProvider, id: '211', name: 'And Data Provider', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', }; - const nestedMultiAndDataProviderMock = set( - 'foo.dataProviders[1].and', - [andDataProvider], - multiDataProviderMock - ); + const nestedMultiAndDataProviderMock = multiDataProviderMock; + multiDataProviderMock.foo.dataProviders[1].and = [andDataProvider]; const update = removeTimelineProvider({ id: 'foo', providerId: '222', timelineById: nestedMultiAndDataProviderMock, }); - expect(update).toEqual( - set( - 'foo.dataProviders', - [ - nestedMultiAndDataProviderMock.foo.dataProviders[0], - { ...andDataProvider, and: [] }, - nestedMultiAndDataProviderMock.foo.dataProviders[2], - ], - timelineByIdMock - ) - ); + expect(update.foo.dataProviders).toEqual([ + nestedMultiAndDataProviderMock.foo.dataProviders[0], + { ...andDataProvider, and: [] }, + nestedMultiAndDataProviderMock.foo.dataProviders[2], + ]); }); test('should remove only the first provider and keep multiple nested andProviders', () => { const multiDataProvider: DataProvider[] = [ { + ...basicDataProvider, and: [ { - enabled: true, + ...basicDataProvider, id: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', name: 'root', - excluded: false, - kqlQuery: '', queryMatch: { field: 'user.name', value: 'root', @@ -2247,11 +1525,9 @@ describe('Timeline', () => { }, }, { - enabled: true, + ...basicDataProvider, id: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', name: 'success', - excluded: false, - kqlQuery: '', queryMatch: { field: 'auditd.result', value: 'success', @@ -2259,11 +1535,8 @@ describe('Timeline', () => { }, }, ], - enabled: true, - excluded: false, id: 'hosts-table-hostName-suricata-iowa', name: 'suricata-iowa', - kqlQuery: '', queryMatch: { field: 'host.name', value: 'suricata-iowa', @@ -2272,7 +1545,13 @@ describe('Timeline', () => { }, ]; - const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock); + const multiDataProviderMock = { + ...timelineByIdMock, + foo: { + ...timelineByIdMock.foo, + dataProviders: multiDataProvider, + }, + }; const update = removeTimelineProvider({ id: 'foo', @@ -2280,51 +1559,40 @@ describe('Timeline', () => { timelineById: multiDataProviderMock, }); - expect(update).toEqual( - set( - 'foo.dataProviders', - [ + expect(update.foo.dataProviders).toEqual([ + { + ...basicDataProvider, + id: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', + name: 'root', + queryMatch: { + field: 'user.name', + value: 'root', + operator: ':', + }, + and: [ { - enabled: true, - id: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', - name: 'root', - excluded: false, - kqlQuery: '', + ...basicDataProvider, + id: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', + name: 'success', queryMatch: { - field: 'user.name', - value: 'root', + field: 'auditd.result', + value: 'success', operator: ':', }, - and: [ - { - enabled: true, - id: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', - name: 'success', - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'auditd.result', - value: 'success', - operator: ':', - }, - }, - ], }, ], - timelineByIdMock - ) - ); + }, + ]); }); test('should remove only the first AND provider when the first AND is deleted, and there are multiple andProviders', () => { const multiDataProvider: DataProvider[] = [ { + ...basicDataProvider, and: [ { - enabled: true, + ...basicDataProvider, id: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', name: 'root', - excluded: false, - kqlQuery: '', queryMatch: { field: 'user.name', value: 'root', @@ -2332,11 +1600,9 @@ describe('Timeline', () => { }, }, { - enabled: true, + ...basicDataProvider, id: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', name: 'success', - excluded: false, - kqlQuery: '', queryMatch: { field: 'auditd.result', value: 'success', @@ -2344,11 +1610,8 @@ describe('Timeline', () => { }, }, ], - enabled: true, - excluded: false, id: 'hosts-table-hostName-suricata-iowa', name: 'suricata-iowa', - kqlQuery: '', queryMatch: { field: 'host.name', value: 'suricata-iowa', @@ -2357,7 +1620,13 @@ describe('Timeline', () => { }, ]; - const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock); + const multiDataProviderMock = { + ...timelineByIdMock, + foo: { + ...timelineByIdMock.foo, + dataProviders: multiDataProvider, + }, + }; const update = removeTimelineProvider({ andProviderId: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', @@ -2366,52 +1635,41 @@ describe('Timeline', () => { timelineById: multiDataProviderMock, }); - expect(update).toEqual( - set( - 'foo.dataProviders', - [ + expect(update.foo.dataProviders).toEqual([ + { + ...basicDataProvider, + and: [ { - and: [ - { - enabled: true, - id: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', - name: 'success', - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'auditd.result', - value: 'success', - operator: ':', - }, - }, - ], - enabled: true, - excluded: false, - id: 'hosts-table-hostName-suricata-iowa', - name: 'suricata-iowa', - kqlQuery: '', + ...basicDataProvider, + id: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', + name: 'success', queryMatch: { - field: 'host.name', - value: 'suricata-iowa', + field: 'auditd.result', + value: 'success', operator: ':', }, }, ], - timelineByIdMock - ) - ); + id: 'hosts-table-hostName-suricata-iowa', + name: 'suricata-iowa', + queryMatch: { + field: 'host.name', + value: 'suricata-iowa', + operator: ':', + }, + }, + ]); }); test('should remove only the second AND provider when the second AND is deleted, and there are multiple andProviders', () => { const multiDataProvider: DataProvider[] = [ { + ...basicDataProvider, and: [ { - enabled: true, + ...basicDataProvider, id: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', name: 'root', - excluded: false, - kqlQuery: '', queryMatch: { field: 'user.name', value: 'root', @@ -2419,11 +1677,9 @@ describe('Timeline', () => { }, }, { - enabled: true, + ...basicDataProvider, id: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', name: 'success', - excluded: false, - kqlQuery: '', queryMatch: { field: 'auditd.result', value: 'success', @@ -2431,11 +1687,8 @@ describe('Timeline', () => { }, }, ], - enabled: true, - excluded: false, id: 'hosts-table-hostName-suricata-iowa', name: 'suricata-iowa', - kqlQuery: '', queryMatch: { field: 'host.name', value: 'suricata-iowa', @@ -2444,7 +1697,13 @@ describe('Timeline', () => { }, ]; - const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock); + const multiDataProviderMock = { + ...timelineByIdMock, + foo: { + ...timelineByIdMock.foo, + dataProviders: multiDataProvider, + }, + }; const update = removeTimelineProvider({ andProviderId: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', @@ -2453,40 +1712,30 @@ describe('Timeline', () => { timelineById: multiDataProviderMock, }); - expect(update).toEqual( - set( - 'foo.dataProviders', - [ + expect(update.foo.dataProviders).toEqual([ + { + ...basicDataProvider, + and: [ { - and: [ - { - enabled: true, - id: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', - name: 'root', - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'user.name', - value: 'root', - operator: ':', - }, - }, - ], - enabled: true, - excluded: false, - id: 'hosts-table-hostName-suricata-iowa', - name: 'suricata-iowa', - kqlQuery: '', + ...basicDataProvider, + id: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', + name: 'root', queryMatch: { - field: 'host.name', - value: 'suricata-iowa', + field: 'user.name', + value: 'root', operator: ':', }, }, ], - timelineByIdMock - ) - ); + id: 'hosts-table-hostName-suricata-iowa', + name: 'suricata-iowa', + queryMatch: { + field: 'host.name', + value: 'suricata-iowa', + operator: ':', + }, + }, + ]); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index d15bce5e217fa..1d956e02e7083 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -44,6 +44,7 @@ import { updateDescription, updateEventType, updateHighlightedDropAndProviderId, + updateIndexNames, updateIsFavorite, updateIsLive, updateIsLoading, @@ -135,6 +136,7 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) show, columns, itemsPerPage, + indexNames, kqlQuery, sort, showCheckboxes, @@ -152,6 +154,7 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) filters, id, itemsPerPage, + indexNames, kqlQuery, sort, show, @@ -521,4 +524,14 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) ...state, insertTimeline, })) + .case(updateIndexNames, (state, { id, indexNames }) => ({ + ...state, + timelineById: { + ...state.timelineById, + [id]: { + ...state.timelineById[id], + indexNames, + }, + }, + })) .build(); diff --git a/x-pack/plugins/security_solution/scripts/beat_docs/build.js b/x-pack/plugins/security_solution/scripts/beat_docs/build.js new file mode 100644 index 0000000000000..9b3607593a5db --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/beat_docs/build.js @@ -0,0 +1,233 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +require('../../../../../src/setup_node_env'); + +// eslint-disable-next-line import/no-extraneous-dependencies +const extract = require('extract-zip'); +const fs = require('fs'); +// eslint-disable-next-line import/no-extraneous-dependencies +const yaml = require('js-yaml'); +const https = require('https'); +// eslint-disable-next-line import/no-extraneous-dependencies +const { get, isArray, isEmpty, isNumber, isString, pick } = require('lodash'); +// eslint-disable-next-line import/no-extraneous-dependencies +const Q = require('q'); +// eslint-disable-next-line import/no-extraneous-dependencies +const rimraf = require('rimraf'); +const { resolve } = require('path'); +// eslint-disable-next-line import/no-extraneous-dependencies +const tar = require('tar'); +const zlib = require('zlib'); + +const OUTPUT_DIRECTORY = resolve('scripts', 'beat_docs'); +const OUTPUT_SERVER_DIRECTORY = resolve('server', 'utils', 'beat_schema'); + +const beats = [ + { + filePath: `${OUTPUT_DIRECTORY}/auditbeat-7.9.0-darwin-x86_64.tar.gz`, + index: 'auditbeat-*', + outputDir: `${OUTPUT_DIRECTORY}/auditbeat-7.9.0-darwin-x86_64`, + url: + 'https://artifacts.elastic.co/downloads/beats/auditbeat/auditbeat-7.9.0-darwin-x86_64.tar.gz', + }, + { + filePath: `${OUTPUT_DIRECTORY}/filebeat-7.9.0-darwin-x86_64.tar.gz`, + index: 'filebeat-*', + outputDir: `${OUTPUT_DIRECTORY}/filebeat-7.9.0-darwin-x86_64`, + url: + 'https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.9.0-darwin-x86_64.tar.gz', + }, + { + filePath: `${OUTPUT_DIRECTORY}/packetbeat-7.9.0-darwin-x86_64.tar.gz`, + index: 'packetbeat-*', + outputDir: `${OUTPUT_DIRECTORY}/packetbeat-7.9.0-darwin-x86_64`, + url: + 'https://artifacts.elastic.co/downloads/beats/packetbeat/packetbeat-7.9.0-darwin-x86_64.tar.gz', + }, + { + filePath: `${OUTPUT_DIRECTORY}/winlogbeat-7.9.0-windows-x86_64.zip`, + index: 'winlogbeat-*', + outputDir: `${OUTPUT_DIRECTORY}`, + url: + 'https://artifacts.elastic.co/downloads/beats/winlogbeat/winlogbeat-7.9.0-windows-x86_64.zip', + }, +]; + +const download = async (url, filepath) => { + const fileStream = fs.createWriteStream(filepath); + const deferred = Q.defer(); + + fileStream + .on('open', function () { + https.get(url, function (res) { + res.on('error', function (err) { + deferred.reject(err); + }); + + res.pipe(fileStream); + }); + }) + .on('error', function (err) { + deferred.reject(err); + }) + .on('finish', function () { + deferred.resolve(filepath); + }); + + return deferred.promise; +}; + +const paramsToPick = ['category', 'description', 'example', 'name', 'type', 'format']; + +const onlyStringOrNumber = (fields) => + Object.keys(fields).reduce((acc, item) => { + let value = get(fields, item); + if (item === 'description' && isString(value)) { + value = value.replace(/\n/g, ' '); + } + return { + ...acc, + [item]: isString(value) || isNumber(value) ? value : JSON.stringify(value), + }; + }, {}); + +const convertFieldsToHash = (schemaFields, beatFields, path) => + schemaFields.fields && isArray(schemaFields.fields) + ? schemaFields.fields.reduce((accumulator, item) => { + if (item.name) { + const attr = isEmpty(path) ? item.name : `${path}.${item.name}`; + const splitAttr = attr.split('.'); + const category = splitAttr.length === 1 ? 'base' : splitAttr[0]; + const myItem = { + ...item, + category, + name: attr, + }; + if (!isEmpty(item.fields)) { + return { + ...accumulator, + ...convertFieldsToHash(myItem, beatFields, attr), + }; + } else if (beatFields[attr] === undefined) { + return { + ...accumulator, + [attr]: onlyStringOrNumber(pick(myItem, paramsToPick)), + }; + } + } + return accumulator; + }, {}) + : {}; + +const convertSchemaToHash = (schema, beatFields) => { + return schema.reduce((accumulator, item) => { + if (item.fields != null && !isEmpty(item.fields)) { + return { + ...accumulator, + ...convertFieldsToHash(item, accumulator), + }; + } + return accumulator; + }, beatFields); +}; + +const manageZipFields = async (beat, filePath, beatFields) => + new Promise((resolve, reject) => { + extract(filePath, { dir: beat.outputDir }, (err) => { + if (err) { + return reject(new Error(err)); + } + console.log('building fields', beat.index); + const obj = yaml.load( + fs.readFileSync(`${beat.outputDir}/winlogbeat-7.9.0-windows-x86_64/fields.yml`, { + encoding: 'utf-8', + }) + ); + const eBeatFields = convertSchemaToHash(obj, beatFields); + console.log('deleting files', beat.index); + rimraf.sync(`${beat.outputDir}/winlogbeat-7.9.0-windows-x86_64`); + rimraf.sync(beat.filePath); + resolve(eBeatFields); + }); + }); + +const manageTarFields = async (beat, filePath, beatFields) => + new Promise((resolve, reject) => { + fs.createReadStream(filePath) + .pipe(zlib.createGunzip()) + .pipe( + tar.extract({ + sync: true, + cwd: OUTPUT_DIRECTORY, + filter: function (path) { + return path.includes('fields.yml'); + }, + }) + ) + .on('end', function (err) { + if (err) { + return reject(new Error(err)); + } + console.log('building fields', beat.index); + const obj = yaml.load( + fs.readFileSync(`${beat.outputDir}/fields.yml`, { encoding: 'utf-8' }) + ); + const ebeatFields = convertSchemaToHash(obj, beatFields); + console.log('deleting files', beat.index); + rimraf.sync(beat.outputDir); + rimraf.sync(beat.filePath); + resolve(ebeatFields); + }); + }); + +async function main() { + let beatFields = { + _id: { + category: 'base', + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + name: '_id', + type: 'keyword', + }, + _index: { + category: 'base', + description: + 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', + example: 'auditbeat-8.0.0-2019.02.19-000001', + name: '_index', + type: 'keyword', + }, + }; + + for (const myBeat of beats) { + console.log('downloading', myBeat.index); + const filepath = await download(myBeat.url, myBeat.filePath); + if (myBeat.index === 'winlogbeat-*') { + beatFields = await manageZipFields(myBeat, filepath, beatFields); + } else { + beatFields = await manageTarFields(myBeat, filepath, beatFields); + } + console.log('done for', myBeat.index); + } + const body = `/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + + import { BeatFields } from '../../../common/search_strategy/security_solution/beat_fields'; + + /* eslint-disable @typescript-eslint/naming-convention */ + export const fieldsBeat: BeatFields = + ${JSON.stringify(beatFields, null, 2)}; + `; + fs.writeFileSync(`${OUTPUT_SERVER_DIRECTORY}/fields.ts`, body, 'utf-8'); +} + +if (require.main === module) { + main(); +} diff --git a/x-pack/plugins/security_solution/server/graphql/source_status/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/source_status/schema.gql.ts index 3062113f1b635..60dc563a3e8d2 100644 --- a/x-pack/plugins/security_solution/server/graphql/source_status/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/source_status/schema.gql.ts @@ -37,6 +37,6 @@ export const sourceStatusSchema = gql` "Whether the configured alias or wildcard pattern resolve to any auditbeat indices" indicesExist(defaultIndex: [String!]!): Boolean! "The list of fields defined in the index mappings" - indexFields(defaultIndex: [String!]!): [IndexField!]! + indexFields(defaultIndex: [String!]!): [String!]! } `; diff --git a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts index 573539e1bb54f..70596a1b41ea0 100644 --- a/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/timeline/schema.gql.ts @@ -167,6 +167,7 @@ export const timelineSchema = gql` filters: [FilterTimelineInput!] kqlMode: String kqlQuery: SerializedFilterQueryInput + indexNames: [String!] title: String templateTimelineId: String templateTimelineVersion: Int @@ -269,6 +270,7 @@ export const timelineSchema = gql` filters: [FilterTimelineResult!] kqlMode: String kqlQuery: SerializedFilterQueryResult + indexNames: [String!] notes: [NoteResult!] noteIds: [String!] pinnedEventIds: [String!] diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 5887feb63c2a1..4c85c08e137fa 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -134,6 +134,8 @@ export interface TimelineInput { kqlQuery?: Maybe; + indexNames?: Maybe; + title?: Maybe; templateTimelineId?: Maybe; @@ -415,10 +417,6 @@ export enum FlowDirection { biDirectional = 'biDirectional', } -export type ToStringArrayNoNullable = any; - -export type ToIFieldSubTypeNonNullable = any; - export type ToStringArray = string[] | string; export type Date = string; @@ -433,6 +431,10 @@ export type ToAny = any; export type EsValue = any; +export type ToStringArrayNoNullable = any; + +export type ToIFieldSubTypeNonNullable = any; + // ==================================================== // Scalars // ==================================================== @@ -591,33 +593,7 @@ export interface SourceStatus { /** Whether the configured alias or wildcard pattern resolve to any auditbeat indices */ indicesExist: boolean; /** The list of fields defined in the index mappings */ - indexFields: IndexField[]; -} - -/** A descriptor of a field in an index */ -export interface IndexField { - /** Where the field belong */ - category: string; - /** Example of field's value */ - example?: Maybe; - /** whether the field's belong to an alias index */ - indexes: (Maybe)[]; - /** The name of the field */ - name: string; - /** The type of the field's values as recognized by Kibana */ - type: string; - /** Whether the field's values can be efficiently searched for */ - searchable: boolean; - /** Whether the field's values can be aggregated */ - aggregatable: boolean; - /** Description of the field */ - description?: Maybe; - - format?: Maybe; - /** the elastic type as mapped in the index */ - esTypes?: Maybe; - - subType?: Maybe; + indexFields: string[]; } export interface AuthenticationsData { @@ -1948,6 +1924,8 @@ export interface TimelineResult { kqlQuery?: Maybe; + indexNames?: Maybe; + notes?: Maybe; noteIds?: Maybe; @@ -2220,6 +2198,32 @@ export interface HostFields { type?: Maybe; } +/** A descriptor of a field in an index */ +export interface IndexField { + /** Where the field belong */ + category: string; + /** Example of field's value */ + example?: Maybe; + /** whether the field's belong to an alias index */ + indexes: (Maybe)[]; + /** The name of the field */ + name: string; + /** The type of the field's values as recognized by Kibana */ + type: string; + /** Whether the field's values can be efficiently searched for */ + searchable: boolean; + /** Whether the field's values can be aggregated */ + aggregatable: boolean; + /** Description of the field */ + description?: Maybe; + + format?: Maybe; + /** the elastic type as mapped in the index */ + esTypes?: Maybe; + + subType?: Maybe; +} + // ==================================================== // Arguments // ==================================================== @@ -3397,7 +3401,7 @@ export namespace SourceStatusResolvers { /** Whether the configured alias or wildcard pattern resolve to any auditbeat indices */ indicesExist?: IndicesExistResolver; /** The list of fields defined in the index mappings */ - indexFields?: IndexFieldsResolver; + indexFields?: IndexFieldsResolver; } export type IndicesExistResolver< @@ -3410,7 +3414,7 @@ export namespace SourceStatusResolvers { } export type IndexFieldsResolver< - R = IndexField[], + R = string[], Parent = SourceStatus, TContext = SiemContext > = Resolver; @@ -3418,89 +3422,6 @@ export namespace SourceStatusResolvers { defaultIndex: string[]; } } -/** A descriptor of a field in an index */ -export namespace IndexFieldResolvers { - export interface Resolvers { - /** Where the field belong */ - category?: CategoryResolver; - /** Example of field's value */ - example?: ExampleResolver, TypeParent, TContext>; - /** whether the field's belong to an alias index */ - indexes?: IndexesResolver<(Maybe)[], TypeParent, TContext>; - /** The name of the field */ - name?: NameResolver; - /** The type of the field's values as recognized by Kibana */ - type?: TypeResolver; - /** Whether the field's values can be efficiently searched for */ - searchable?: SearchableResolver; - /** Whether the field's values can be aggregated */ - aggregatable?: AggregatableResolver; - /** Description of the field */ - description?: DescriptionResolver, TypeParent, TContext>; - - format?: FormatResolver, TypeParent, TContext>; - /** the elastic type as mapped in the index */ - esTypes?: EsTypesResolver, TypeParent, TContext>; - - subType?: SubTypeResolver, TypeParent, TContext>; - } - - export type CategoryResolver = Resolver< - R, - Parent, - TContext - >; - export type ExampleResolver< - R = Maybe, - Parent = IndexField, - TContext = SiemContext - > = Resolver; - export type IndexesResolver< - R = (Maybe)[], - Parent = IndexField, - TContext = SiemContext - > = Resolver; - export type NameResolver = Resolver< - R, - Parent, - TContext - >; - export type TypeResolver = Resolver< - R, - Parent, - TContext - >; - export type SearchableResolver< - R = boolean, - Parent = IndexField, - TContext = SiemContext - > = Resolver; - export type AggregatableResolver< - R = boolean, - Parent = IndexField, - TContext = SiemContext - > = Resolver; - export type DescriptionResolver< - R = Maybe, - Parent = IndexField, - TContext = SiemContext - > = Resolver; - export type FormatResolver< - R = Maybe, - Parent = IndexField, - TContext = SiemContext - > = Resolver; - export type EsTypesResolver< - R = Maybe, - Parent = IndexField, - TContext = SiemContext - > = Resolver; - export type SubTypeResolver< - R = Maybe, - Parent = IndexField, - TContext = SiemContext - > = Resolver; -} export namespace AuthenticationsDataResolvers { export interface Resolvers { @@ -7925,6 +7846,8 @@ export namespace TimelineResultResolvers { kqlQuery?: KqlQueryResolver, TypeParent, TContext>; + indexNames?: IndexNamesResolver, TypeParent, TContext>; + notes?: NotesResolver, TypeParent, TContext>; noteIds?: NoteIdsResolver, TypeParent, TContext>; @@ -8025,6 +7948,11 @@ export namespace TimelineResultResolvers { Parent = TimelineResult, TContext = SiemContext > = Resolver; + export type IndexNamesResolver< + R = Maybe, + Parent = TimelineResult, + TContext = SiemContext + > = Resolver; export type NotesResolver< R = Maybe, Parent = TimelineResult, @@ -8973,6 +8901,89 @@ export namespace HostFieldsResolvers { TContext = SiemContext > = Resolver; } +/** A descriptor of a field in an index */ +export namespace IndexFieldResolvers { + export interface Resolvers { + /** Where the field belong */ + category?: CategoryResolver; + /** Example of field's value */ + example?: ExampleResolver, TypeParent, TContext>; + /** whether the field's belong to an alias index */ + indexes?: IndexesResolver<(Maybe)[], TypeParent, TContext>; + /** The name of the field */ + name?: NameResolver; + /** The type of the field's values as recognized by Kibana */ + type?: TypeResolver; + /** Whether the field's values can be efficiently searched for */ + searchable?: SearchableResolver; + /** Whether the field's values can be aggregated */ + aggregatable?: AggregatableResolver; + /** Description of the field */ + description?: DescriptionResolver, TypeParent, TContext>; + + format?: FormatResolver, TypeParent, TContext>; + /** the elastic type as mapped in the index */ + esTypes?: EsTypesResolver, TypeParent, TContext>; + + subType?: SubTypeResolver, TypeParent, TContext>; + } + + export type CategoryResolver = Resolver< + R, + Parent, + TContext + >; + export type ExampleResolver< + R = Maybe, + Parent = IndexField, + TContext = SiemContext + > = Resolver; + export type IndexesResolver< + R = (Maybe)[], + Parent = IndexField, + TContext = SiemContext + > = Resolver; + export type NameResolver = Resolver< + R, + Parent, + TContext + >; + export type TypeResolver = Resolver< + R, + Parent, + TContext + >; + export type SearchableResolver< + R = boolean, + Parent = IndexField, + TContext = SiemContext + > = Resolver; + export type AggregatableResolver< + R = boolean, + Parent = IndexField, + TContext = SiemContext + > = Resolver; + export type DescriptionResolver< + R = Maybe, + Parent = IndexField, + TContext = SiemContext + > = Resolver; + export type FormatResolver< + R = Maybe, + Parent = IndexField, + TContext = SiemContext + > = Resolver; + export type EsTypesResolver< + R = Maybe, + Parent = IndexField, + TContext = SiemContext + > = Resolver; + export type SubTypeResolver< + R = Maybe, + Parent = IndexField, + TContext = SiemContext + > = Resolver; +} /** Directs the executor to skip this field or fragment when the `if` argument is true. */ export type SkipDirectiveResolver = DirectiveResolverFn< @@ -9007,14 +9018,6 @@ export interface DeprecatedDirectiveArgs { reason?: string; } -export interface ToStringArrayNoNullableScalarConfig - extends GraphQLScalarTypeConfig { - name: 'ToStringArrayNoNullable'; -} -export interface ToIFieldSubTypeNonNullableScalarConfig - extends GraphQLScalarTypeConfig { - name: 'ToIFieldSubTypeNonNullable'; -} export interface ToStringArrayScalarConfig extends GraphQLScalarTypeConfig { name: 'ToStringArray'; } @@ -9036,6 +9039,14 @@ export interface ToAnyScalarConfig extends GraphQLScalarTypeConfig { export interface EsValueScalarConfig extends GraphQLScalarTypeConfig { name: 'EsValue'; } +export interface ToStringArrayNoNullableScalarConfig + extends GraphQLScalarTypeConfig { + name: 'ToStringArrayNoNullable'; +} +export interface ToIFieldSubTypeNonNullableScalarConfig + extends GraphQLScalarTypeConfig { + name: 'ToIFieldSubTypeNonNullable'; +} export type IResolvers = { Query?: QueryResolvers.Resolvers; @@ -9046,7 +9057,6 @@ export type IResolvers = { SourceConfiguration?: SourceConfigurationResolvers.Resolvers; SourceFields?: SourceFieldsResolvers.Resolvers; SourceStatus?: SourceStatusResolvers.Resolvers; - IndexField?: IndexFieldResolvers.Resolvers; AuthenticationsData?: AuthenticationsDataResolvers.Resolvers; AuthenticationsEdges?: AuthenticationsEdgesResolvers.Resolvers; AuthenticationItem?: AuthenticationItemResolvers.Resolvers; @@ -9182,8 +9192,7 @@ export type IResolvers = { EventsTimelineData?: EventsTimelineDataResolvers.Resolvers; OsFields?: OsFieldsResolvers.Resolvers; HostFields?: HostFieldsResolvers.Resolvers; - ToStringArrayNoNullable?: GraphQLScalarType; - ToIFieldSubTypeNonNullable?: GraphQLScalarType; + IndexField?: IndexFieldResolvers.Resolvers; ToStringArray?: GraphQLScalarType; Date?: GraphQLScalarType; ToNumberArray?: GraphQLScalarType; @@ -9191,6 +9200,8 @@ export type IResolvers = { ToBooleanArray?: GraphQLScalarType; ToAny?: GraphQLScalarType; EsValue?: GraphQLScalarType; + ToStringArrayNoNullable?: GraphQLScalarType; + ToIFieldSubTypeNonNullable?: GraphQLScalarType; } & { [typeName: string]: never }; export type IDirectiveResolvers = { diff --git a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts index 430ada93b4514..cfd7bfbf255f6 100644 --- a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts +++ b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts @@ -45,7 +45,7 @@ export function compose( const domainLibs: AppDomainLibs = { authentications: new Authentications(new ElasticsearchAuthenticationAdapter(framework)), events: new Events(new ElasticsearchEventsAdapter(framework)), - fields: new IndexFields(new ElasticsearchIndexFieldAdapter(framework)), + fields: new IndexFields(new ElasticsearchIndexFieldAdapter()), hosts: new Hosts(new ElasticsearchHostsAdapter(framework, endpointContext)), ipDetails: new IpDetails(new ElasticsearchIpDetailsAdapter(framework)), kpiHosts: new KpiHosts(new ElasticsearchKpiHostsAdapter(framework)), diff --git a/x-pack/plugins/security_solution/server/lib/events/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/events/elasticsearch_adapter.ts index dda52e26ca42b..8b656272ecc99 100644 --- a/x-pack/plugins/security_solution/server/lib/events/elasticsearch_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/events/elasticsearch_adapter.ts @@ -26,7 +26,6 @@ import { TimelineDetailsData, TimelineEdges, } from '../../graphql/types'; -import { baseCategoryFields } from '../../utils/beat_schema/8.0.0'; import { reduceFields } from '../../utils/build_query/reduce_fields'; import { mergeFieldsWithHit, inspectStringifyObject } from '../../utils/build_query'; import { eventFieldsMap } from '../ecs_fields'; @@ -44,6 +43,8 @@ import { TimelineRequestOptions, } from './types'; +const baseCategoryFields = ['@timestamp', 'labels', 'message', 'tags']; + export class ElasticsearchEventsAdapter implements EventsAdapter { constructor(private readonly framework: FrameworkAdapter) {} diff --git a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts index 777b1cf3bb80d..6cfa13bfd2a7a 100644 --- a/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/index_fields/elasticsearch_adapter.ts @@ -4,167 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty } from 'lodash/fp'; - -import { IndexField } from '../../graphql/types'; -import { baseCategoryFields, getDocumentation, hasDocumentation } from '../../utils/beat_schema'; -import { FrameworkAdapter, FrameworkRequest } from '../framework'; -import { FieldsAdapter, IndexFieldDescriptor } from './types'; +import { FrameworkRequest } from '../framework'; +import { FieldsAdapter } from './types'; export class ElasticsearchIndexFieldAdapter implements FieldsAdapter { - constructor(private readonly framework: FrameworkAdapter) {} - public async getIndexFields(request: FrameworkRequest, indices: string[]): Promise { - const indexPatternsService = this.framework.getIndexPatternsService(request); - const responsesIndexFields = await Promise.all( - indices.map((index) => { - return indexPatternsService.getFieldsForWildcard({ - pattern: index, - }); - }) - ); - return formatIndexFields(responsesIndexFields, indices); + // Deprecated until we delete all the code + public async getIndexFields(request: FrameworkRequest, indices: string[]): Promise { + return Promise.resolve(['deprecated']); } } - -const missingFields = [ - { - name: '_id', - type: 'string', - searchable: true, - aggregatable: false, - readFromDocValues: true, - }, - { - name: '_index', - type: 'string', - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, -]; - -/** - * Creates a single field item. - * - * This is a mutatious HOT CODE PATH function that will have array sizes up to 4.7 megs - * in size at a time calling this function repeatedly. This function should be as optimized as possible - * and should avoid any and all creation of new arrays, iterating over the arrays or performing - * any n^2 operations. - * @param indexesAlias The index alias - * @param index The index its self - * @param indexesAliasIdx The index within the alias - */ -export const createFieldItem = ( - indexesAlias: string[], - index: IndexFieldDescriptor, - indexesAliasIdx: number -): IndexField => { - const alias = indexesAlias[indexesAliasIdx]; - const splitName = index.name.split('.'); - const category = baseCategoryFields.includes(splitName[0]) ? 'base' : splitName[0]; - return { - ...(hasDocumentation(alias, index.name) ? getDocumentation(alias, index.name) : {}), - ...index, - category, - indexes: [alias], - }; -}; - -/** - * This is a mutatious HOT CODE PATH function that will have array sizes up to 4.7 megs - * in size at a time when being called. This function should be as optimized as possible - * and should avoid any and all creation of new arrays, iterating over the arrays or performing - * any n^2 operations. The `.push`, and `forEach` operations are expected within this function - * to speed up performance. - * - * This intentionally waits for the next tick on the event loop to process as the large 4.7 megs - * has already consumed a lot of the event loop processing up to this function and we want to give - * I/O opportunity to occur by scheduling this on the next loop. - * @param responsesIndexFields The response index fields to loop over - * @param indexesAlias The index aliases such as filebeat-* - */ -export const formatFirstFields = async ( - responsesIndexFields: IndexFieldDescriptor[][], - indexesAlias: string[] -): Promise => { - return new Promise((resolve) => { - setTimeout(() => { - resolve( - responsesIndexFields.reduce( - ( - accumulator: IndexField[], - indexFields: IndexFieldDescriptor[], - indexesAliasIdx: number - ) => { - missingFields.forEach((index) => { - const item = createFieldItem(indexesAlias, index, indexesAliasIdx); - accumulator.push(item); - }); - indexFields.forEach((index) => { - const item = createFieldItem(indexesAlias, index, indexesAliasIdx); - accumulator.push(item); - }); - return accumulator; - }, - [] - ) - ); - }); - }); -}; - -/** - * This is a mutatious HOT CODE PATH function that will have array sizes up to 4.7 megs - * in size at a time when being called. This function should be as optimized as possible - * and should avoid any and all creation of new arrays, iterating over the arrays or performing - * any n^2 operations. The `.push`, and `forEach` operations are expected within this function - * to speed up performance. The "indexFieldNameHash" side effect hash avoids additional expensive n^2 - * look ups. - * - * This intentionally waits for the next tick on the event loop to process as the large 4.7 megs - * has already consumed a lot of the event loop processing up to this function and we want to give - * I/O opportunity to occur by scheduling this on the next loop. - * @param fields The index fields to create the secondary fields for - */ -export const formatSecondFields = async (fields: IndexField[]): Promise => { - return new Promise((resolve) => { - setTimeout(() => { - const indexFieldNameHash: Record = {}; - const reduced = fields.reduce((accumulator: IndexField[], indexfield: IndexField) => { - const alreadyExistingIndexField = indexFieldNameHash[indexfield.name]; - if (alreadyExistingIndexField != null) { - const existingIndexField = accumulator[alreadyExistingIndexField]; - if (isEmpty(accumulator[alreadyExistingIndexField].description)) { - accumulator[alreadyExistingIndexField].description = indexfield.description; - } - accumulator[alreadyExistingIndexField].indexes = Array.from( - new Set([...existingIndexField.indexes, ...indexfield.indexes]) - ); - return accumulator; - } - accumulator.push(indexfield); - indexFieldNameHash[indexfield.name] = accumulator.length - 1; - return accumulator; - }, []); - resolve(reduced); - }); - }); -}; - -/** - * Formats the index fields into a format the UI wants. - * - * NOTE: This will have array sizes up to 4.7 megs in size at a time when being called. - * This function should be as optimized as possible and should avoid any and all creation - * of new arrays, iterating over the arrays or performing any n^2 operations. - * @param responsesIndexFields The response index fields to format - * @param indexesAlias The index alias - */ -export const formatIndexFields = async ( - responsesIndexFields: IndexFieldDescriptor[][], - indexesAlias: string[] -): Promise => { - const fields = await formatFirstFields(responsesIndexFields, indexesAlias); - const secondFields = await formatSecondFields(fields); - return secondFields; -}; diff --git a/x-pack/plugins/security_solution/server/lib/index_fields/index.ts b/x-pack/plugins/security_solution/server/lib/index_fields/index.ts index a3ea8548bddc2..94966bc16a407 100644 --- a/x-pack/plugins/security_solution/server/lib/index_fields/index.ts +++ b/x-pack/plugins/security_solution/server/lib/index_fields/index.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IndexField } from '../../graphql/types'; - import { FieldsAdapter } from './types'; import { FrameworkRequest } from '../framework'; export { ElasticsearchIndexFieldAdapter } from './elasticsearch_adapter'; @@ -13,7 +11,8 @@ export { ElasticsearchIndexFieldAdapter } from './elasticsearch_adapter'; export class IndexFields { constructor(private readonly adapter: FieldsAdapter) {} - public async getFields(request: FrameworkRequest, defaultIndex: string[]): Promise { + // Deprecated until we delete all the code + public async getFields(request: FrameworkRequest, defaultIndex: string[]): Promise { return this.adapter.getIndexFields(request, defaultIndex); } } diff --git a/x-pack/plugins/security_solution/server/lib/index_fields/types.ts b/x-pack/plugins/security_solution/server/lib/index_fields/types.ts index 67b3c254007e2..fdc3509d0d452 100644 --- a/x-pack/plugins/security_solution/server/lib/index_fields/types.ts +++ b/x-pack/plugins/security_solution/server/lib/index_fields/types.ts @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IndexField } from '../../graphql/types'; import { FrameworkRequest } from '../framework'; import { IFieldSubType } from '../../../../../../src/plugins/data/common'; export interface FieldsAdapter { - getIndexFields(req: FrameworkRequest, indices: string[]): Promise; + getIndexFields(req: FrameworkRequest, indices: string[]): Promise; } export interface IndexFieldDescriptor { diff --git a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts index c5ee611dfa27f..11082cd7295cc 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/saved_object_mappings.ts @@ -213,6 +213,9 @@ export const timelineSavedObjectMappings: SavedObjectsType['mappings'] = { }, }, }, + indexNames: { + type: 'text', + }, kqlMode: { type: 'keyword', }, diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 0571c4878956f..22dbd623930c5 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -62,6 +62,7 @@ import { initUsageCollectors } from './usage'; import { AppRequestContext } from './types'; import { registerTrustedAppsRoutes } from './endpoint/routes/trusted_apps'; import { securitySolutionSearchStrategyProvider } from './search_strategy/security_solution'; +import { securitySolutionIndexFieldsProvider } from './search_strategy/index_fields'; import { securitySolutionTimelineSearchStrategyProvider } from './search_strategy/timeline'; export interface SetupPlugins { @@ -277,10 +278,16 @@ export class Plugin implements IPlugin { @@ -29,7 +24,7 @@ describe('Index Fields', () => { sortBy('name', [ { description: - 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + 'Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.', example: '2016-05-23T08:05:34.853Z', name: '@timestamp', type: 'date', @@ -37,41 +32,37 @@ describe('Index Fields', () => { aggregatable: true, category: 'base', indexes: ['auditbeat', 'filebeat', 'packetbeat'], + readFromDocValues: true, + esTypes: [], }, { description: 'Each document has an _id that uniquely identifies it', example: 'Y-6TfmcB0WOhS6qyMv3s', - footnote: '', - group: 1, - level: 'core', name: '_id', - required: true, type: 'string', searchable: true, aggregatable: false, - readFromDocValues: true, - category: '_id', + readFromDocValues: false, + category: 'base', indexes: ['auditbeat', 'filebeat', 'packetbeat'], + esTypes: [], }, { description: 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', example: 'auditbeat-8.0.0-2019.02.19-000001', - footnote: '', - group: 1, - level: 'core', name: '_index', - required: true, type: 'string', searchable: true, aggregatable: true, - readFromDocValues: true, - category: '_index', + readFromDocValues: false, + category: 'base', indexes: ['auditbeat', 'filebeat', 'packetbeat'], + esTypes: [], }, { description: - 'Ephemeral identifier of this agent (if one exists).\n\nThis id normally changes across restarts, but `agent.id` does not.', + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', example: '8a4f500f', name: 'agent.ephemeral_id', type: 'string', @@ -79,18 +70,24 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['auditbeat'], + readFromDocValues: false, + esTypes: [], }, { + description: + 'Deprecated - use agent.name or agent.id to identify an agent. Hostname of the agent. ', name: 'agent.hostname', searchable: true, type: 'string', aggregatable: true, category: 'agent', indexes: ['filebeat'], + readFromDocValues: false, + esTypes: [], }, { description: - 'Unique identifier of this agent (if one exists).\n\nExample: For Beats this would be beat.id.', + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', example: '8a4f500d', name: 'agent.id', type: 'string', @@ -98,10 +95,12 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['packetbeat'], + readFromDocValues: false, + esTypes: [], }, { description: - 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', + 'Custom name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', example: 'foo', name: 'agent.name', type: 'string', @@ -109,10 +108,12 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['auditbeat', 'filebeat'], + readFromDocValues: false, + esTypes: [], }, { description: - 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', + 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', example: 'filebeat', name: 'agent.type', type: 'string', @@ -120,6 +121,8 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['auditbeat', 'packetbeat'], + readFromDocValues: false, + esTypes: [], }, { description: 'Version of the agent.', @@ -130,6 +133,8 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['auditbeat', 'filebeat'], + readFromDocValues: false, + esTypes: [], }, ]) ); @@ -146,37 +151,31 @@ describe('Index Fields', () => { { description: 'Each document has an _id that uniquely identifies it', example: 'Y-6TfmcB0WOhS6qyMv3s', - footnote: '', - group: 1, - level: 'core', name: '_id', - required: true, type: 'string', searchable: true, aggregatable: false, - readFromDocValues: true, - category: '_id', + readFromDocValues: false, + category: 'base', indexes: ['auditbeat'], + esTypes: [], }, { description: 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', example: 'auditbeat-8.0.0-2019.02.19-000001', - footnote: '', - group: 1, - level: 'core', name: '_index', - required: true, type: 'string', searchable: true, aggregatable: true, - readFromDocValues: true, - category: '_index', + readFromDocValues: false, + category: 'base', indexes: ['auditbeat'], + esTypes: [], }, { description: - 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + 'Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.', example: '2016-05-23T08:05:34.853Z', name: '@timestamp', type: 'date', @@ -184,10 +183,12 @@ describe('Index Fields', () => { aggregatable: true, category: 'base', indexes: ['auditbeat'], + readFromDocValues: true, + esTypes: [], }, { description: - 'Ephemeral identifier of this agent (if one exists).\n\nThis id normally changes across restarts, but `agent.id` does not.', + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', example: '8a4f500f', name: 'agent.ephemeral_id', type: 'string', @@ -195,10 +196,12 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['auditbeat'], + readFromDocValues: false, + esTypes: [], }, { description: - 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', + 'Custom name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', example: 'foo', name: 'agent.name', type: 'string', @@ -206,10 +209,12 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['auditbeat'], + readFromDocValues: false, + esTypes: [], }, { description: - 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', + 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', example: 'filebeat', name: 'agent.type', type: 'string', @@ -217,6 +222,8 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['auditbeat'], + readFromDocValues: false, + esTypes: [], }, { description: 'Version of the agent.', @@ -227,41 +234,37 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['auditbeat'], + readFromDocValues: false, + esTypes: [], }, { description: 'Each document has an _id that uniquely identifies it', example: 'Y-6TfmcB0WOhS6qyMv3s', - footnote: '', - group: 1, - level: 'core', name: '_id', - required: true, type: 'string', searchable: true, aggregatable: false, - readFromDocValues: true, - category: '_id', + category: 'base', indexes: ['filebeat'], + readFromDocValues: false, + esTypes: [], }, { description: 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', example: 'auditbeat-8.0.0-2019.02.19-000001', - footnote: '', - group: 1, - level: 'core', name: '_index', - required: true, type: 'string', searchable: true, aggregatable: true, - readFromDocValues: true, - category: '_index', + category: 'base', indexes: ['filebeat'], + readFromDocValues: false, + esTypes: [], }, { description: - 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + 'Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.', example: '2016-05-23T08:05:34.853Z', name: '@timestamp', type: 'date', @@ -269,18 +272,24 @@ describe('Index Fields', () => { aggregatable: true, category: 'base', indexes: ['filebeat'], + readFromDocValues: true, + esTypes: [], }, { + description: + 'Deprecated - use agent.name or agent.id to identify an agent. Hostname of the agent. ', name: 'agent.hostname', searchable: true, type: 'string', aggregatable: true, category: 'agent', indexes: ['filebeat'], + readFromDocValues: false, + esTypes: [], }, { description: - 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', + 'Custom name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', example: 'foo', name: 'agent.name', type: 'string', @@ -288,6 +297,8 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['filebeat'], + readFromDocValues: false, + esTypes: [], }, { description: 'Version of the agent.', @@ -298,41 +309,37 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['filebeat'], + readFromDocValues: false, + esTypes: [], }, { description: 'Each document has an _id that uniquely identifies it', example: 'Y-6TfmcB0WOhS6qyMv3s', - footnote: '', - group: 1, - level: 'core', name: '_id', - required: true, type: 'string', searchable: true, aggregatable: false, - readFromDocValues: true, - category: '_id', + category: 'base', indexes: ['packetbeat'], + readFromDocValues: false, + esTypes: [], }, { description: 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', example: 'auditbeat-8.0.0-2019.02.19-000001', - footnote: '', - group: 1, - level: 'core', name: '_index', - required: true, type: 'string', searchable: true, aggregatable: true, - readFromDocValues: true, - category: '_index', + category: 'base', indexes: ['packetbeat'], + readFromDocValues: false, + esTypes: [], }, { description: - 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + 'Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.', example: '2016-05-23T08:05:34.853Z', name: '@timestamp', type: 'date', @@ -340,10 +347,12 @@ describe('Index Fields', () => { aggregatable: true, category: 'base', indexes: ['packetbeat'], + readFromDocValues: true, + esTypes: [], }, { description: - 'Unique identifier of this agent (if one exists).\n\nExample: For Beats this would be beat.id.', + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', example: '8a4f500d', name: 'agent.id', type: 'string', @@ -351,10 +360,12 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['packetbeat'], + readFromDocValues: false, + esTypes: [], }, { description: - 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', + 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', example: 'filebeat', name: 'agent.type', type: 'string', @@ -362,6 +373,8 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['packetbeat'], + readFromDocValues: false, + esTypes: [], }, ]); }); @@ -377,8 +390,9 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: false, - category: '_id', + category: 'base', indexes: ['auditbeat'], + readFromDocValues: false, }, { description: @@ -388,12 +402,13 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: '_index', + category: 'base', indexes: ['auditbeat'], + readFromDocValues: false, }, { description: - 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + 'Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.', example: '2016-05-23T08:05:34.853Z', name: '@timestamp', type: 'date', @@ -401,10 +416,11 @@ describe('Index Fields', () => { aggregatable: true, category: 'base', indexes: ['auditbeat'], + readFromDocValues: true, }, { description: - 'Ephemeral identifier of this agent (if one exists).\n\nThis id normally changes across restarts, but `agent.id` does not.', + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', example: '8a4f500f', name: 'agent.ephemeral_id', type: 'string', @@ -412,10 +428,11 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['auditbeat'], + readFromDocValues: false, }, { description: - 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', + 'Custom name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', example: 'foo', name: 'agent.name', type: 'string', @@ -423,10 +440,11 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['auditbeat'], + readFromDocValues: false, }, { description: - 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', + 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', example: 'filebeat', name: 'agent.type', type: 'string', @@ -434,6 +452,7 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['auditbeat'], + readFromDocValues: false, }, { description: 'Version of the agent.', @@ -444,6 +463,7 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['auditbeat'], + readFromDocValues: false, }, { description: 'Each document has an _id that uniquely identifies it', @@ -452,8 +472,9 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: false, - category: '_id', + category: 'base', indexes: ['filebeat'], + readFromDocValues: false, }, { description: @@ -463,12 +484,13 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: '_index', + category: 'base', indexes: ['filebeat'], + readFromDocValues: false, }, { description: - 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + 'Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.', example: '2016-05-23T08:05:34.853Z', name: '@timestamp', type: 'date', @@ -476,6 +498,7 @@ describe('Index Fields', () => { aggregatable: true, category: 'base', indexes: ['filebeat'], + readFromDocValues: true, }, { name: 'agent.hostname', @@ -484,10 +507,11 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['filebeat'], + readFromDocValues: false, }, { description: - 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', + 'Custom name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', example: 'foo', name: 'agent.name', type: 'string', @@ -495,6 +519,7 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['filebeat'], + readFromDocValues: false, }, { description: 'Version of the agent.', @@ -505,6 +530,7 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['filebeat'], + readFromDocValues: false, }, { description: 'Each document has an _id that uniquely identifies it', @@ -513,8 +539,9 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: false, - category: '_id', + category: 'base', indexes: ['packetbeat'], + readFromDocValues: false, }, { description: @@ -524,12 +551,13 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: '_index', + category: 'base', indexes: ['packetbeat'], + readFromDocValues: false, }, { description: - 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + 'Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.', example: '2016-05-23T08:05:34.853Z', name: '@timestamp', type: 'date', @@ -537,10 +565,11 @@ describe('Index Fields', () => { aggregatable: true, category: 'base', indexes: ['packetbeat'], + readFromDocValues: true, }, { description: - 'Unique identifier of this agent (if one exists).\n\nExample: For Beats this would be beat.id.', + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', example: '8a4f500d', name: 'agent.id', type: 'string', @@ -548,10 +577,11 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['packetbeat'], + readFromDocValues: false, }, { description: - 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', + 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', example: 'filebeat', name: 'agent.type', type: 'string', @@ -559,6 +589,7 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['packetbeat'], + readFromDocValues: false, }, ]); expect(fields).toEqual([ @@ -569,8 +600,9 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: false, - category: '_id', + category: 'base', indexes: ['auditbeat', 'filebeat', 'packetbeat'], + readFromDocValues: false, }, { description: @@ -580,12 +612,13 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: true, - category: '_index', + category: 'base', indexes: ['auditbeat', 'filebeat', 'packetbeat'], + readFromDocValues: false, }, { description: - 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', + 'Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.', example: '2016-05-23T08:05:34.853Z', name: '@timestamp', type: 'date', @@ -593,10 +626,11 @@ describe('Index Fields', () => { aggregatable: true, category: 'base', indexes: ['auditbeat', 'filebeat', 'packetbeat'], + readFromDocValues: true, }, { description: - 'Ephemeral identifier of this agent (if one exists).\n\nThis id normally changes across restarts, but `agent.id` does not.', + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', example: '8a4f500f', name: 'agent.ephemeral_id', type: 'string', @@ -604,10 +638,11 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['auditbeat'], + readFromDocValues: false, }, { description: - 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', + 'Custom name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', example: 'foo', name: 'agent.name', type: 'string', @@ -615,10 +650,11 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['auditbeat', 'filebeat'], + readFromDocValues: false, }, { description: - 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', + 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', example: 'filebeat', name: 'agent.type', type: 'string', @@ -626,6 +662,7 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['auditbeat', 'packetbeat'], + readFromDocValues: false, }, { description: 'Version of the agent.', @@ -636,6 +673,7 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['auditbeat', 'filebeat'], + readFromDocValues: false, }, { name: 'agent.hostname', @@ -644,10 +682,11 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['filebeat'], + readFromDocValues: false, }, { description: - 'Unique identifier of this agent (if one exists).\n\nExample: For Beats this would be beat.id.', + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', example: '8a4f500d', name: 'agent.id', type: 'string', @@ -655,6 +694,7 @@ describe('Index Fields', () => { aggregatable: true, category: 'agent', indexes: ['packetbeat'], + readFromDocValues: false, }, ]); }); @@ -669,22 +709,22 @@ describe('Index Fields', () => { type: 'string', searchable: true, aggregatable: false, + readFromDocValues: false, + esTypes: [], }, 0 ); expect(item).toEqual({ description: 'Each document has an _id that uniquely identifies it', example: 'Y-6TfmcB0WOhS6qyMv3s', - footnote: '', - group: 1, - level: 'core', name: '_id', - required: true, type: 'string', searchable: true, aggregatable: false, - category: '_id', + category: 'base', indexes: ['auditbeat'], + readFromDocValues: false, + esTypes: [], }); }); }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts b/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.ts new file mode 100644 index 0000000000000..403a9425b221f --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/index_fields/index.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import isEmpty from 'lodash/isEmpty'; +import { IndexPatternsFetcher, ISearchStrategy } from '../../../../../../src/plugins/data/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FieldDescriptor } from '../../../../../../src/plugins/data/server/index_patterns'; +import { + IndexFieldsStrategyResponse, + IndexField, + IndexFieldsStrategyRequest, +} from '../../../common/search_strategy/index_fields'; + +import { fieldsBeat } from '../../utils/beat_schema/fields'; + +export const securitySolutionIndexFieldsProvider = (): ISearchStrategy< + IndexFieldsStrategyRequest, + IndexFieldsStrategyResponse +> => { + return { + search: async (context, request) => { + const { elasticsearch } = context.core; + const indexPatternsFetcher = new IndexPatternsFetcher( + elasticsearch.legacy.client.callAsCurrentUser + ); + const dedupeIndices = dedupeIndexName(request.indices); + + const responsesIndexFields = await Promise.all( + dedupeIndices + .map((index) => + indexPatternsFetcher.getFieldsForWildcard({ + pattern: index, + }) + ) + .map((p) => p.catch((e) => false)) + ); + let indexFields: IndexField[] = []; + + if (!request.onlyCheckIfIndicesExist) { + indexFields = await formatIndexFields( + responsesIndexFields.filter((rif) => rif !== false) as FieldDescriptor[][], + dedupeIndices + ); + } + + return Promise.resolve({ + indexFields, + indicesExist: dedupeIndices.filter((index, i) => responsesIndexFields[i] !== false), + rawResponse: { + timed_out: false, + took: -1, + _shards: { + total: -1, + successful: -1, + failed: -1, + skipped: -1, + }, + hits: { + total: -1, + max_score: -1, + hits: [ + { + _index: '', + _type: '', + _id: '', + _score: -1, + _source: null, + }, + ], + }, + }, + }); + }, + }; +}; + +export const dedupeIndexName = (indices: string[]) => + indices.reduce((acc, index) => { + if (index.trim() !== '' && index.trim() !== '_all' && !acc.includes(index.trim())) { + return [...acc, index]; + } + return acc; + }, []); + +const missingFields: FieldDescriptor[] = [ + { + name: '_id', + type: 'string', + searchable: true, + aggregatable: false, + readFromDocValues: false, + esTypes: [], + }, + { + name: '_index', + type: 'string', + searchable: true, + aggregatable: true, + readFromDocValues: false, + esTypes: [], + }, +]; + +/** + * Creates a single field item. + * + * This is a mutatious HOT CODE PATH function that will have array sizes up to 4.7 megs + * in size at a time calling this function repeatedly. This function should be as optimized as possible + * and should avoid any and all creation of new arrays, iterating over the arrays or performing + * any n^2 operations. + * @param indexesAlias The index alias + * @param index The index its self + * @param indexesAliasIdx The index within the alias + */ +export const createFieldItem = ( + indexesAlias: string[], + index: FieldDescriptor, + indexesAliasIdx: number +): IndexField => { + const alias = indexesAlias[indexesAliasIdx]; + return { + ...(fieldsBeat[index.name] ?? {}), + ...index, + indexes: [alias], + }; +}; + +/** + * This is a mutatious HOT CODE PATH function that will have array sizes up to 4.7 megs + * in size at a time when being called. This function should be as optimized as possible + * and should avoid any and all creation of new arrays, iterating over the arrays or performing + * any n^2 operations. The `.push`, and `forEach` operations are expected within this function + * to speed up performance. + * + * This intentionally waits for the next tick on the event loop to process as the large 4.7 megs + * has already consumed a lot of the event loop processing up to this function and we want to give + * I/O opportunity to occur by scheduling this on the next loop. + * @param responsesIndexFields The response index fields to loop over + * @param indexesAlias The index aliases such as filebeat-* + */ +export const formatFirstFields = async ( + responsesIndexFields: FieldDescriptor[][], + indexesAlias: string[] +): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + resolve( + responsesIndexFields.reduce( + (accumulator: IndexField[], indexFields: FieldDescriptor[], indexesAliasIdx: number) => { + missingFields.forEach((index) => { + const item = createFieldItem(indexesAlias, index, indexesAliasIdx); + accumulator.push(item); + }); + indexFields.forEach((index) => { + const item = createFieldItem(indexesAlias, index, indexesAliasIdx); + accumulator.push(item); + }); + return accumulator; + }, + [] + ) + ); + }); + }); +}; + +/** + * This is a mutatious HOT CODE PATH function that will have array sizes up to 4.7 megs + * in size at a time when being called. This function should be as optimized as possible + * and should avoid any and all creation of new arrays, iterating over the arrays or performing + * any n^2 operations. The `.push`, and `forEach` operations are expected within this function + * to speed up performance. The "indexFieldNameHash" side effect hash avoids additional expensive n^2 + * look ups. + * + * This intentionally waits for the next tick on the event loop to process as the large 4.7 megs + * has already consumed a lot of the event loop processing up to this function and we want to give + * I/O opportunity to occur by scheduling this on the next loop. + * @param fields The index fields to create the secondary fields for + */ +export const formatSecondFields = async (fields: IndexField[]): Promise => { + return new Promise((resolve) => { + setTimeout(() => { + const indexFieldNameHash: Record = {}; + const reduced = fields.reduce((accumulator: IndexField[], indexfield: IndexField) => { + const alreadyExistingIndexField = indexFieldNameHash[indexfield.name]; + if (alreadyExistingIndexField != null) { + const existingIndexField = accumulator[alreadyExistingIndexField]; + if (isEmpty(accumulator[alreadyExistingIndexField].description)) { + accumulator[alreadyExistingIndexField].description = indexfield.description; + } + accumulator[alreadyExistingIndexField].indexes = Array.from( + new Set([...existingIndexField.indexes, ...indexfield.indexes]) + ); + return accumulator; + } + accumulator.push(indexfield); + indexFieldNameHash[indexfield.name] = accumulator.length - 1; + return accumulator; + }, []); + resolve(reduced); + }); + }); +}; + +/** + * Formats the index fields into a format the UI wants. + * + * NOTE: This will have array sizes up to 4.7 megs in size at a time when being called. + * This function should be as optimized as possible and should avoid any and all creation + * of new arrays, iterating over the arrays or performing any n^2 operations. + * @param responsesIndexFields The response index fields to format + * @param indexesAlias The index alias + */ +export const formatIndexFields = async ( + responsesIndexFields: FieldDescriptor[][], + indexesAlias: string[] +): Promise => { + const fields = await formatFirstFields(responsesIndexFields, indexesAlias); + const secondFields = await formatSecondFields(fields); + return secondFields; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/index_fields/mock.ts b/x-pack/plugins/security_solution/server/search_strategy/index_fields/mock.ts new file mode 100644 index 0000000000000..efb992a868f65 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/index_fields/mock.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FieldDescriptor } from '../../../../../../src/plugins/data/server/index_patterns'; + +export const mockAuditbeatIndexField: FieldDescriptor[] = [ + { + name: '@timestamp', + searchable: true, + type: 'date', + aggregatable: true, + readFromDocValues: true, + esTypes: [], + }, + { + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + aggregatable: true, + readFromDocValues: false, + esTypes: [], + }, + { + name: 'agent.name', + searchable: true, + type: 'string', + aggregatable: true, + readFromDocValues: false, + esTypes: [], + }, + { + name: 'agent.type', + searchable: true, + type: 'string', + aggregatable: true, + readFromDocValues: false, + esTypes: [], + }, + { + name: 'agent.version', + searchable: true, + type: 'string', + aggregatable: true, + readFromDocValues: false, + esTypes: [], + }, +]; + +export const mockFilebeatIndexField: FieldDescriptor[] = [ + { + name: '@timestamp', + searchable: true, + type: 'date', + aggregatable: true, + readFromDocValues: true, + esTypes: [], + }, + { + name: 'agent.hostname', + searchable: true, + type: 'string', + aggregatable: true, + readFromDocValues: false, + esTypes: [], + }, + { + name: 'agent.name', + searchable: true, + type: 'string', + aggregatable: true, + readFromDocValues: false, + esTypes: [], + }, + { + name: 'agent.version', + searchable: true, + type: 'string', + aggregatable: true, + readFromDocValues: false, + esTypes: [], + }, +]; + +export const mockPacketbeatIndexField: FieldDescriptor[] = [ + { + name: '@timestamp', + searchable: true, + type: 'date', + aggregatable: true, + readFromDocValues: true, + esTypes: [], + }, + { + name: 'agent.id', + searchable: true, + type: 'string', + aggregatable: true, + readFromDocValues: false, + esTypes: [], + }, + { + name: 'agent.type', + searchable: true, + type: 'string', + aggregatable: true, + readFromDocValues: false, + esTypes: [], + }, +]; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts index edff22766cc54..b2ce57d87ae6d 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts @@ -19,6 +19,7 @@ export const formatTimelineData = ( flattenedFields.node._id = hit._id; flattenedFields.node._index = hit._index; flattenedFields.node.ecs._id = hit._id; + flattenedFields.node.ecs.timestamp = hit._source['@timestamp']; flattenedFields.node.ecs._index = hit._index; if (hit.sort && hit.sort.length > 1) { flattenedFields.cursor.value = hit.sort[0]; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts index 3b0935db9a5d6..2dd406ffaa450 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts @@ -7,7 +7,8 @@ import { get, isEmpty, isNumber, isObject, isString } from 'lodash/fp'; import { TimelineEventsDetailsItem } from '../../../../../../common/search_strategy/timeline'; -import { baseCategoryFields } from '../../../../../utils/beat_schema/8.0.0'; + +export const baseCategoryFields = ['@timestamp', 'labels', 'message', 'tags']; export const getFieldCategory = (field: string): string => { const fieldCategory = field.split('.')[0]; diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/auditbeat.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/auditbeat.ts deleted file mode 100644 index 76c865679dd05..0000000000000 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/auditbeat.ts +++ /dev/null @@ -1,7902 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -/** - * An instance of the unmodified schema exported from auditbeat-8.0.0-SNAPSHOT-darwin-x86_64.tar.gz - * - */ - -import { Schema } from '../type'; - -export const auditbeatSchema: Schema = [ - { - key: 'ecs', - title: 'ECS', - description: 'ECS Fields.', - fields: [ - { - name: '@timestamp', - level: 'core', - required: true, - type: 'date', - description: - 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', - example: '2016-05-23T08:05:34.853Z', - }, - { - name: 'labels', - level: 'core', - type: 'object', - object_type: 'keyword', - description: - 'Custom key/value pairs.\n\nCan be used to add meta information to events. Should not contain nested objects.\nAll values are stored as keyword.\n\nExample: `docker` and `k8s` labels.', - example: '{"application": "foo-bar", "env": "production"}', - }, - { - name: 'message', - level: 'core', - type: 'text', - description: - 'For log events the message field contains the log message, optimized\nfor viewing in a log viewer.\n\nFor structured logs without an original message field, other fields can be concatenated\nto form a human-readable summary of the event.\n\nIf multiple messages exist, they can be combined into one message.', - example: 'Hello World', - }, - { - name: 'tags', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'List of keywords used to tag each event.', - example: '["production", "env2"]', - }, - { - name: 'agent', - title: 'Agent', - group: 2, - description: - 'The agent fields contain the data about the software entity, if\nany, that collects, detects, or observes events on a host, or takes measurements\non a host.\n\nExamples include Beats. Agents may also run on observers. ECS agent.* fields\nshall be populated with details of the agent running on the host or observer\nwhere the event happened or the measurement was taken.', - footnote: - 'Examples: In the case of Beats for logs, the agent.name is filebeat.\nFor APM, it is the agent running in the app/service. The agent information does\nnot change if data is sent through queuing systems like Kafka, Redis, or processing\nsystems such as Logstash or APM Server.', - type: 'group', - fields: [ - { - name: 'ephemeral_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Ephemeral identifier of this agent (if one exists).\n\nThis id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier of this agent (if one exists).\n\nExample: For Beats this would be beat.id.', - example: '8a4f500d', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', - example: 'foo', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', - example: 'filebeat', - }, - { - name: 'version', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Version of the agent.', - example: '6.0.0-rc2', - }, - ], - }, - { - name: 'as', - title: 'Autonomous System', - group: 2, - description: - 'An autonomous system (AS) is a collection of connected Internet Protocol\n(IP) routing prefixes under the control of one or more network operators on\nbehalf of a single administrative entity or domain that presents a common, clearly\ndefined routing policy to the internet.', - type: 'group', - fields: [ - { - name: 'number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - ], - }, - { - name: 'client', - title: 'Client', - group: 2, - description: - 'A client is defined as the initiator of a network connection for\nevents regarding sessions, connections, or bidirectional flow records.\n\nFor TCP events, the client is the initiator of the TCP connection that sends\nthe SYN packet(s). For other protocols, the client is generally the initiator\nor requestor in the network transaction. Some systems use the term "originator"\nto refer the client in TCP connections. The client fields describe details about\nthe system acting as the client in the network event. Client fields are usually\npopulated in conjunction with server fields. Client fields are generally not\npopulated for packet-level events.\n\nClient / server representations can add semantic context to an exchange, which\nis helpful to visualize the data in certain situations. If your context falls\nin that category, you should still ensure that source and destination are filled\nappropriately.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Some event client addresses are defined ambiguously. The event\nwill sometimes list an IP, a domain or a unix socket. You should always store\nthe raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'as.number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'as.organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the client to the server.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Client domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: - 'IP address of the client.\n\nCan be one or multiple IPv4 or IPv6 addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC address of the client.', - }, - { - name: 'nat.ip', - level: 'extended', - type: 'ip', - description: - 'Translated IP of source based NAT sessions (e.g. internal client\nto internet).\n\nTypically connections traversing load balancers, firewalls, or routers.', - }, - { - name: 'nat.port', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Translated port of source based NAT sessions (e.g. internal client\nto internet).\n\nTypically connections traversing load balancers, firewalls, or routers.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the client to the server.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the client.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered client domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'cloud', - title: 'Cloud', - group: 2, - description: 'Fields related to the cloud or infrastructure the events are coming\nfrom.', - footnote: - 'Examples: If Metricbeat is running on an EC2 host and fetches data\nfrom its host, the cloud info contains the data about this machine. If Metricbeat\nruns on a remote machine outside the cloud and fetches data from a service running\nin the cloud, the field contains cloud data from the machine the service is\nrunning on.', - type: 'group', - fields: [ - { - name: 'account.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The cloud account or organization id used to identify different\nentities in a multi-tenant environment.\n\nExamples: AWS account id, Google Cloud ORG Id, or other unique identifier.', - example: 666777888999, - }, - { - name: 'availability_zone', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Availability zone in which this host is running.', - example: 'us-east-1c', - }, - { - name: 'instance.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Instance ID of the host machine.', - example: 'i-1234567890abcdef0', - }, - { - name: 'instance.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Instance name of the host machine.', - }, - { - name: 'machine.type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Machine type of the host machine.', - example: 't2.medium', - }, - { - name: 'provider', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the cloud provider. Example values are aws, azure, gcp,\nor digitalocean.', - example: 'aws', - }, - { - name: 'region', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Region in which this host is running.', - example: 'us-east-1', - }, - ], - }, - { - name: 'code_signature', - title: 'Code Signature', - group: 2, - description: 'These fields contain information about binary code signatures.', - type: 'group', - fields: [ - { - name: 'exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - ], - }, - { - name: 'container', - title: 'Container', - group: 2, - description: - 'Container fields are used for meta information about the specific\ncontainer that is the source of information.\n\nThese fields help correlate data based containers from any runtime.', - type: 'group', - fields: [ - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique container id.', - }, - { - name: 'image.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the image the container was built on.', - }, - { - name: 'image.tag', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Container image tags.', - }, - { - name: 'labels', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: 'Image labels.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Container name.', - }, - { - name: 'runtime', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Runtime managing this container.', - example: 'docker', - }, - ], - }, - { - name: 'destination', - title: 'Destination', - group: 2, - description: - 'Destination fields describe details about the destination of a packet/event.\n\nDestination fields are usually populated in conjunction with source fields.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Some event destination addresses are defined ambiguously. The\nevent will sometimes list an IP, a domain or a unix socket. You should always\nstore the raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'as.number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'as.organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the destination to the source.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Destination domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: - 'IP address of the destination.\n\nCan be one or multiple IPv4 or IPv6 addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC address of the destination.', - }, - { - name: 'nat.ip', - level: 'extended', - type: 'ip', - description: - 'Translated ip of destination based NAT sessions (e.g. internet\nto private DMZ)\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'nat.port', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Port the source session is translated to by NAT Device.\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the destination to the source.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the destination.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered destination domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'dll', - title: 'DLL', - group: 2, - description: - 'These fields contain information about code libraries dynamically\nloaded into processes.\n\n\nMany operating systems refer to "shared code libraries" with different names,\nbut this field set refers to all of the following:\n\n* Dynamic-link library (`.dll`) commonly used on Windows\n\n* Shared Object (`.so`) commonly used on Unix-like operating systems\n\n* Dynamic library (`.dylib`) commonly used on macOS', - type: 'group', - fields: [ - { - name: 'code_signature.exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'code_signature.subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'code_signature.trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - { - name: 'hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - default_field: false, - }, - { - name: 'hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - default_field: false, - }, - { - name: 'hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - default_field: false, - }, - { - name: 'hash.sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - default_field: false, - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the library.\n\nThis generally maps to the name of the file on disk.', - example: 'kernel32.dll', - default_field: false, - }, - { - name: 'path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Full file path of the library.', - example: 'C:\\Windows\\System32\\kernel32.dll', - default_field: false, - }, - { - name: 'pe.company', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal company name of the file, provided at compile-time.', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'pe.description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal description of the file, provided at compile-time.', - example: 'Paint', - default_field: false, - }, - { - name: 'pe.file_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal version of the file, provided at compile-time.', - example: '6.3.9600.17415', - default_field: false, - }, - { - name: 'pe.original_file_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal name of the file, provided at compile-time.', - example: 'MSPAINT.EXE', - default_field: false, - }, - { - name: 'pe.product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal product name of the file, provided at compile-time.', - example: 'Microsoft® Windows® Operating System', - default_field: false, - }, - ], - }, - { - name: 'dns', - title: 'DNS', - group: 2, - description: - 'Fields describing DNS queries and answers.\n\nDNS events should either represent a single DNS query prior to getting answers\n(`dns.type:query`) or they should represent a full exchange and contain the\nquery details as well as all of the answers that were provided for this query\n(`dns.type:answer`).', - type: 'group', - fields: [ - { - name: 'answers', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'An array containing an object for each answer section returned\nby the server.\n\nThe main keys that should be present in these objects are defined by ECS.\nRecords that have more information may contain more keys than what ECS defines.\n\nNot all DNS data sources give all details about DNS answers. At minimum, answer\nobjects must contain the `data` key. If more information is available, map\nas much of it to ECS as possible, and add any additional fields to the answer\nobjects as custom fields.', - }, - { - name: 'answers.class', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The class of DNS data contained in this resource record.', - example: 'IN', - }, - { - name: 'answers.data', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The data describing the resource.\n\nThe meaning of this data depends on the type and class of the resource record.', - example: '10.10.10.10', - }, - { - name: 'answers.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The domain name to which this resource record pertains.\n\nIf a chain of CNAME is being resolved, each answer `name` should be the\none that corresponds with the answer `data`. It should not simply be the\noriginal `question.name` repeated.', - example: 'www.google.com', - }, - { - name: 'answers.ttl', - level: 'extended', - type: 'long', - description: - 'The time interval in seconds that this resource record may be cached\nbefore it should be discarded. Zero values mean that the data should not be\ncached.', - example: 180, - }, - { - name: 'answers.type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The type of data contained in this resource record.', - example: 'CNAME', - }, - { - name: 'header_flags', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of 2 letter DNS header flags.\n\nExpected values are: AA, TC, RD, RA, AD, CD, DO.', - example: ['RD', 'RA'], - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The DNS packet identifier assigned by the program that generated\nthe query. The identifier is copied to the response.', - example: 62111, - }, - { - name: 'op_code', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The DNS operation code that specifies the kind of query in the\nmessage. This value is set by the originator of a query and copied into the\nresponse.', - example: 'QUERY', - }, - { - name: 'question.class', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The class of records being queried.', - example: 'IN', - }, - { - name: 'question.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The name being queried.\n\nIf the name field contains non-printable characters (below 32 or above 126),\nthose characters should be represented as escaped base 10 integers (\\DDD).\nBack slashes and quotes should be escaped. Tabs, carriage returns, and line\nfeeds should be converted to \\t, \\r, and \\n respectively.', - example: 'www.google.com', - }, - { - name: 'question.registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'question.subdomain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The subdomain is all of the labels under the registered_domain.\n\nIf the domain has multiple levels of subdomain, such as "sub2.sub1.example.com",\nthe subdomain field should contain "sub2.sub1", with no trailing period.', - example: 'www', - }, - { - name: 'question.top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'question.type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The type of record being queried.', - example: 'AAAA', - }, - { - name: 'resolved_ip', - level: 'extended', - type: 'ip', - description: - 'Array containing all IPs seen in `answers.data`.\n\nThe `answers` array can be difficult to use, because of the variety of data\nformats it can contain. Extracting all IP addresses seen in there to `dns.resolved_ip`\nmakes it possible to index them as IP addresses, and makes them easier to\nvisualize and query for.', - example: ['10.10.10.10', '10.10.10.11'], - }, - { - name: 'response_code', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The DNS response code.', - example: 'NOERROR', - }, - { - name: 'type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of DNS event captured, query or answer.\n\nIf your source of DNS events only gives you DNS queries, you should only create\ndns events of type `dns.type:query`.\n\nIf your source of DNS events gives you answers as well, you should create\none event per query (optionally as soon as the query is seen). And a second\nevent containing all query details as well as an array of answers.', - example: 'answer', - }, - ], - }, - { - name: 'ecs', - title: 'ECS', - group: 2, - description: 'Meta-information specific to ECS.', - type: 'group', - fields: [ - { - name: 'version', - level: 'core', - required: true, - type: 'keyword', - ignore_above: 1024, - description: - 'ECS version this event conforms to. `ecs.version` is a required\nfield and must exist in all events.\n\nWhen querying across multiple indices -- which may conform to slightly different\nECS versions -- this field lets integrations adjust to the schema version\nof the events.', - example: '1.0.0', - }, - ], - }, - { - name: 'error', - title: 'Error', - group: 2, - description: - 'These fields can represent errors of any kind.\n\nUse them for errors that happen while fetching events or in cases where the\nevent itself contains an error.', - type: 'group', - fields: [ - { - name: 'code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Error code describing the error.', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the error.', - }, - { - name: 'message', - level: 'core', - type: 'text', - description: 'Error message.', - }, - { - name: 'stack_trace', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'The stack trace of this error in plain text.', - }, - { - name: 'type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The type of the error, for example the class name of the exception.', - example: 'java.lang.NullPointerException', - }, - ], - }, - { - name: 'event', - title: 'Event', - group: 2, - description: - 'The event fields are used for context information about the log\nor metric event itself.\n\nA log is defined as an event containing details of something that happened.\nLog events must include the time at which the thing happened. Examples of log\nevents include a process starting on a host, a network packet being sent from\na source to a destination, or a network connection between a client and a server\nbeing initiated or closed. A metric is defined as an event containing one or\nmore numerical measurements and the time at which the measurement was taken.\nExamples of metric events include memory pressure measured on a host and device\ntemperature. See the `event.kind` definition in this section for additional\ndetails about metric and state events.', - type: 'group', - fields: [ - { - name: 'action', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.', - example: 'user-password-change', - }, - { - name: 'category', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is one of four ECS Categorization Fields, and indicates the\nsecond level in the ECS category hierarchy.\n\n`event.category` represents the "big buckets" of ECS categories. For example,\nfiltering on `event.category:process` yields all events relating to process\nactivity. This field is closely related to `event.type`, which is used as\na subcategory.\n\nThis field is an array. This will allow proper categorization of some events\nthat fall in multiple categories.', - example: 'authentication', - }, - { - name: 'code', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Identification code for this event, if one exists.\n\nSome event sources use event codes to identify messages unambiguously, regardless\nof message language or wording adjustments over time. An example of this is\nthe Windows Event ID.', - example: 4648, - }, - { - name: 'created', - level: 'core', - type: 'date', - description: - 'event.created contains the date/time when the event was first\nread by an agent, or by your pipeline.\n\nThis field is distinct from @timestamp in that @timestamp typically contain\nthe time extracted from the original event.\n\nIn most situations, these two timestamps will be slightly different. The difference\ncan be used to calculate the delay between your source generating an event,\nand the time when your agent first processed it. This can be used to monitor\nyour agent or pipeline ability to keep up with your event source.\n\nIn case the two timestamps are identical, @timestamp should be used.', - example: '2016-05-23T08:05:34.857Z', - }, - { - name: 'dataset', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the dataset.\n\nIf an event source publishes more than one type of log or events (e.g. access\nlog, error log), the dataset is used to specify which one the event comes\nfrom.\n\nIt is recommended but not required to start the dataset name with the module\nname, followed by a dot, then the dataset name.', - example: 'apache.access', - }, - { - name: 'duration', - level: 'core', - type: 'long', - format: 'duration', - input_format: 'nanoseconds', - output_format: 'asMilliseconds', - output_precision: 1, - description: - 'Duration of the event in nanoseconds.\n\nIf event.start and event.end are known this value should be the difference\nbetween the end and start time.', - }, - { - name: 'end', - level: 'extended', - type: 'date', - description: - 'event.end contains the date when the event ended or when the activity\nwas last observed.', - }, - { - name: 'hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Hash (perhaps logstash fingerprint) of raw field to be able to\ndemonstrate log integrity.', - example: '123456789012345678901234567890ABCD', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique ID to describe the event.', - example: '8a4f500d', - }, - { - name: 'ingested', - level: 'core', - type: 'date', - description: - 'Timestamp when an event arrived in the central data store.\n\nThis is different from `@timestamp`, which is when the event originally occurred. It is\nalso different from `event.created`, which is meant to capture the first time\nan agent saw the event.\n\nIn normal conditions, assuming no tampering, the timestamps should chronologically\nlook like this: `@timestamp` < `event.created` < `event.ingested`.', - example: '2016-05-23T08:05:35.101Z', - default_field: false, - }, - { - name: 'kind', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is one of four ECS Categorization Fields, and indicates the\nhighest level in the ECS category hierarchy.\n\n`event.kind` gives high-level information about what type of information the\nevent contains, without being specific to the contents of the event. For example,\nvalues of this field distinguish alert events from metric events.\n\nThe value of this field can be used to inform how these kinds of events should\nbe handled. They may warrant different retention, different access control,\nit may also help understand whether the data coming in at a regular interval\nor not.', - example: 'alert', - }, - { - name: 'module', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the module this data is coming from.\n\nIf your monitoring agent supports the concept of modules or plugins to process\nevents of a given source (e.g. Apache logs), `event.module` should contain\nthe name of this module.', - example: 'apache', - }, - { - name: 'original', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Raw text message of entire event. Used to demonstrate log integrity.\n\nThis field is not indexed and doc_values are disabled. It cannot be searched,\nbut it can be retrieved from `_source`.', - example: - 'Sep 19 08:26:10 host CEF:0|Security| threatmanager|1.0|100|\nworm successfully stopped|10|src=10.0.0.1 dst=2.1.2.2spt=1232', - }, - { - name: 'outcome', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is one of four ECS Categorization Fields, and indicates the\nlowest level in the ECS category hierarchy.\n\n`event.outcome` simply denotes whether the event represents a success or a\nfailure from the perspective of the entity that produced the event.\n\nNote that when a single transaction is described in multiple events, each\nevent may populate different values of `event.outcome`, according to their\nperspective.\n\nAlso note that in the case of a compound event (a single event that contains\nmultiple logical events), this field should be populated with the value that\nbest captures the overall success or failure from the perspective of the event\nproducer.\n\nFurther note that not all events will have an associated outcome. For example,\nthis field is generally not populated for metric events, events with `event.type:info`,\nor any events for which an outcome does not make logical sense.', - example: 'success', - }, - { - name: 'provider', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Source of the event.\n\nEvent transports such as Syslog or the Windows Event Log typically mention\nthe source of an event. It can be the name of the software that generated\nthe event (e.g. Sysmon, httpd), or of a subsystem of the operating system\n(kernel, Microsoft-Windows-Security-Auditing).', - example: 'kernel', - }, - { - name: 'reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Reference URL linking to additional information about this event.\n\nThis URL links to a static definition of the this event. Alert events, indicated\nby `event.kind:alert`, are a common use case for this field.', - example: 'https://system.vendor.com/event/#0001234', - default_field: false, - }, - { - name: 'risk_score', - level: 'core', - type: 'float', - description: - "Risk score or priority of the event (e.g. security solutions).\nUse your system's original value here.", - }, - { - name: 'risk_score_norm', - level: 'extended', - type: 'float', - description: - 'Normalized risk score or priority of the event, on a scale of\n0 to 100.\n\nThis is mainly useful if you use more than one system that assigns risk scores,\nand you want to see a normalized value across all systems.', - }, - { - name: 'sequence', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Sequence number of the event.\n\nThe sequence number is a value published by some event sources, to make the\nexact ordering of events unambiguous, regardless of the timestamp precision.', - }, - { - name: 'severity', - level: 'core', - type: 'long', - format: 'string', - description: - 'The numeric severity of the event according to your event source.\n\nWhat the different severity values mean can be different between sources and\nuse cases. It is up to the implementer to make sure severities are consistent\nacross events from the same source.\n\nThe Syslog severity belongs in `log.syslog.severity.code`. `event.severity`\nis meant to represent the severity according to the event source (e.g. firewall,\nIDS). If the event source does not publish its own severity, you may optionally\ncopy the `log.syslog.severity.code` to `event.severity`.', - example: 7, - }, - { - name: 'start', - level: 'extended', - type: 'date', - description: - 'event.start contains the date when the event started or when the\nactivity was first observed.', - }, - { - name: 'timezone', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'This field should be populated when the event timestamp does\nnot include timezone information already (e.g. default Syslog timestamps).\nIt is optional otherwise.\n\nAcceptable timezone formats are: a canonical ID (e.g. "Europe/Amsterdam"),\nabbreviated (e.g. "EST") or an HH:mm differential (e.g. "-05:00").', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is one of four ECS Categorization Fields, and indicates the\nthird level in the ECS category hierarchy.\n\n`event.type` represents a categorization "sub-bucket" that, when used along\nwith the `event.category` field values, enables filtering events down to a\nlevel appropriate for single visualization.\n\nThis field is an array. This will allow proper categorization of some events\nthat fall in multiple event types.', - }, - { - name: 'url', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'URL linking to an external system to continue investigation of\nthis event.\n\nThis URL links to another system where in-depth investigation of the specific\noccurence of this event can take place. Alert events, indicated by `event.kind:alert`,\nare a common use case for this field.', - example: 'https://mysystem.mydomain.com/alert/5271dedb-f5b0-4218-87f0-4ac4870a38fe', - default_field: false, - }, - ], - }, - { - name: 'file', - title: 'File', - group: 2, - description: - 'A file is defined as a set of information that has been created\non, or has existed on a filesystem.\n\nFile objects can be associated with host events, network events, and/or file\nevents (e.g., those produced by File Integrity Monitoring [FIM] products or\nservices). File fields provide details about the affected file associated with\nthe event or metric.', - type: 'group', - fields: [ - { - name: 'accessed', - level: 'extended', - type: 'date', - description: - 'Last time the file was accessed.\n\nNote that not all filesystems keep track of access time.', - }, - { - name: 'attributes', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of file attributes.\n\nAttributes names will vary by platform. Here is a non-exhaustive list of values\nthat are expected in this field: archive, compressed, directory, encrypted,\nexecute, hidden, read, readonly, system, write.', - example: '["readonly", "system"]', - default_field: false, - }, - { - name: 'code_signature.exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'code_signature.subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'code_signature.trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - { - name: 'created', - level: 'extended', - type: 'date', - description: - 'File creation time.\n\nNote that not all filesystems store the creation time.', - }, - { - name: 'ctime', - level: 'extended', - type: 'date', - description: - 'Last time the file attributes or metadata changed.\n\nNote that changes to the file content will update `mtime`. This implies `ctime`\nwill be adjusted at the same time, since `mtime` is an attribute of the file.', - }, - { - name: 'device', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Device that is the source of the file.', - example: 'sda', - }, - { - name: 'directory', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Directory where the file is located. It should include the drive\nletter, when appropriate.', - example: '/home/alice', - }, - { - name: 'drive_letter', - level: 'extended', - type: 'keyword', - ignore_above: 1, - description: - 'Drive letter where the file is located. This field is only relevant\non Windows.\n\nThe value should be uppercase, and not include the colon.', - example: 'C', - default_field: false, - }, - { - name: 'extension', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'File extension.', - example: 'png', - }, - { - name: 'gid', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Primary group ID (GID) of the file.', - example: '1001', - }, - { - name: 'group', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Primary group name of the file.', - example: 'alice', - }, - { - name: 'hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - }, - { - name: 'hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - }, - { - name: 'hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - }, - { - name: 'hash.sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - }, - { - name: 'inode', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Inode representing the file in the filesystem.', - example: '256383', - }, - { - name: 'mime_type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'MIME type should identify the format of the file or stream of bytes\nusing https://www.iana.org/assignments/media-types/media-types.xhtml[IANA\nofficial types], where possible. When more than one type is applicable, the\nmost specific type should be used.', - default_field: false, - }, - { - name: 'mode', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Mode of the file in octal representation.', - example: '0640', - }, - { - name: 'mtime', - level: 'extended', - type: 'date', - description: 'Last time the file content was modified.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the file including the extension, without the directory.', - example: 'example.png', - }, - { - name: 'owner', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: "File owner's username.", - example: 'alice', - }, - { - name: 'path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'Full path to the file, including the file name. It should include\nthe drive letter, when appropriate.', - example: '/home/alice/example.png', - }, - { - name: 'pe.company', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal company name of the file, provided at compile-time.', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'pe.description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal description of the file, provided at compile-time.', - example: 'Paint', - default_field: false, - }, - { - name: 'pe.file_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal version of the file, provided at compile-time.', - example: '6.3.9600.17415', - default_field: false, - }, - { - name: 'pe.original_file_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal name of the file, provided at compile-time.', - example: 'MSPAINT.EXE', - default_field: false, - }, - { - name: 'pe.product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal product name of the file, provided at compile-time.', - example: 'Microsoft® Windows® Operating System', - default_field: false, - }, - { - name: 'size', - level: 'extended', - type: 'long', - description: 'File size in bytes.\n\nOnly relevant when `file.type` is "file".', - example: 16384, - }, - { - name: 'target_path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Target path for symlinks.', - }, - { - name: 'type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'File type (file, dir, or symlink).', - example: 'file', - }, - { - name: 'uid', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The user ID (UID) or security identifier (SID) of the file owner.', - example: '1001', - }, - ], - }, - { - name: 'geo', - title: 'Geo', - group: 2, - description: - 'Geo fields can carry data about a specific location related to an\nevent.\n\nThis geolocation information can be derived from techniques such as Geo IP,\nor be user-supplied.', - type: 'group', - fields: [ - { - name: 'city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - ], - }, - { - name: 'group', - title: 'Group', - group: 2, - description: - 'The group fields are meant to represent groups that are relevant\nto the event.', - type: 'group', - fields: [ - { - name: 'domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - ], - }, - { - name: 'hash', - title: 'Hash', - group: 2, - description: - 'The hash fields represent different hash algorithms and their values.\n\nField names for common hashes (e.g. MD5, SHA1) are predefined. Add fields for\nother hashes by lowercasing the hash algorithm name and using underscore separators\nas appropriate (snake case, e.g. sha3_512).', - type: 'group', - fields: [ - { - name: 'md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - }, - { - name: 'sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - }, - { - name: 'sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - }, - { - name: 'sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - }, - ], - }, - { - name: 'host', - title: 'Host', - group: 2, - description: - 'A host is defined as a general computing instance.\n\nECS host.* fields should be populated with details about the host on which the\nevent happened, or from which the measurement was taken. Host types include\nhardware, virtual machines, Docker containers, and Kubernetes nodes.', - type: 'group', - fields: [ - { - name: 'architecture', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system architecture.', - example: 'x86_64', - }, - { - name: 'domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the domain of which the host is a member.\n\nFor example, on Windows this could be the host Active Directory domain\nor NetBIOS domain name. For Linux this could be the domain of the host\nLDAP provider.', - example: 'CONTOSO', - default_field: false, - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'hostname', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Hostname of the host.\n\nIt normally contains what the `hostname` command returns on the host machine.', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique host id.\n\nAs hostname is not always unique, use values that are meaningful in your environment.\n\nExample: The current usage of `beat.name`.', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: 'Host ip addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Host mac addresses.', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.', - }, - { - name: 'os.family', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'os.full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'os.kernel', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'os.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'os.platform', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'os.version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Type of host.\n\nFor Cloud providers this can be the machine type like `t2.medium`. If vm,\nthis could be the container, for example, or other information meaningful\nin your environment.', - }, - { - name: 'uptime', - level: 'extended', - type: 'long', - description: 'Seconds the host has been up.', - example: 1325, - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'http', - title: 'HTTP', - group: 2, - description: - 'Fields related to HTTP activity. Use the `url` field set to store\nthe url of the request.', - type: 'group', - fields: [ - { - name: 'request.body.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Size in bytes of the request body.', - example: 887, - }, - { - name: 'request.body.content', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'The full HTTP request body.', - example: 'Hello world', - }, - { - name: 'request.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Total size in bytes of the request (body and headers).', - example: 1437, - }, - { - name: 'request.method', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'HTTP request method.\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'get, post, put', - }, - { - name: 'request.referrer', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Referrer for this HTTP request.', - example: 'https://blog.example.com/', - }, - { - name: 'response.body.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Size in bytes of the response body.', - example: 887, - }, - { - name: 'response.body.content', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'The full HTTP response body.', - example: 'Hello world', - }, - { - name: 'response.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Total size in bytes of the response (body and headers).', - example: 1437, - }, - { - name: 'response.status_code', - level: 'extended', - type: 'long', - format: 'string', - description: 'HTTP response status code.', - example: 404, - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'HTTP version.', - example: 1.1, - }, - ], - }, - { - name: 'interface', - title: 'Interface', - group: 2, - description: - 'The interface fields are used to record ingress and egress interface\ninformation when reported by an observer (e.g. firewall, router, load balancer)\nin the context of the observer handling a network connection. In the case of\na single observer interface (e.g. network sensor on a span port) only the observer.ingress\ninformation should be populated.', - type: 'group', - fields: [ - { - name: 'alias', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Interface alias as reported by the system, typically used in firewall\nimplementations for e.g. inside, outside, or dmz logical interface naming.', - example: 'outside', - default_field: false, - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface ID as reported by an observer (typically SNMP interface\nID).', - example: 10, - default_field: false, - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface name as reported by the system.', - example: 'eth0', - default_field: false, - }, - ], - }, - { - name: 'log', - title: 'Log', - group: 2, - description: - 'Details about the event logging mechanism or logging transport.\n\nThe log.* fields are typically populated with details about the logging mechanism\nused to create and/or transport the event. For example, syslog details belong\nunder `log.syslog.*`.\n\nThe details specific to your event source are typically not logged under `log.*`,\nbut rather in `event.*` or in other ECS fields.', - type: 'group', - fields: [ - { - name: 'level', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Original log level of the log event.\n\nIf the source of the event provides a log level or textual severity, this\nis the one that goes in `log.level`. If your source does not specify one,\nyou may put your event transport severity here (e.g. Syslog severity).\n\nSome examples are `warn`, `err`, `i`, `informational`.', - example: 'error', - }, - { - name: 'logger', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'The name of the logger inside an application. This is usually the\nname of the class which initialized the logger, or can be a custom name.', - example: 'org.elasticsearch.bootstrap.Bootstrap', - }, - { - name: 'origin.file.line', - level: 'extended', - type: 'integer', - description: - 'The line number of the file containing the source code which originated\nthe log event.', - example: 42, - }, - { - name: 'origin.file.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The name of the file containing the source code which originated\nthe log event. Note that this is not the name of the log file.', - example: 'Bootstrap.java', - }, - { - name: 'origin.function', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The name of the function or method which originated the log event.', - example: 'init', - }, - { - name: 'original', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is the original log message and contains the full log message\nbefore splitting it up in multiple parts.\n\nIn contrast to the `message` field which can contain an extracted part of\nthe log message, this field contains the original, full log message. It can\nhave already some modifications applied like encoding or new lines removed\nto clean up the log message.\n\nThis field is not indexed and doc_values are disabled so it cannot be queried\nbut the value can be retrieved from `_source`.', - example: 'Sep 19 08:26:10 localhost My log', - }, - { - name: 'syslog', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'The Syslog metadata of the event, if the event was transmitted\nvia Syslog. Please see RFCs 5424 or 3164.', - }, - { - name: 'syslog.facility.code', - level: 'extended', - type: 'long', - format: 'string', - description: - 'The Syslog numeric facility of the log event, if available.\n\nAccording to RFCs 5424 and 3164, this value should be an integer between 0\nand 23.', - example: 23, - }, - { - name: 'syslog.facility.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The Syslog text-based facility of the log event, if available.', - example: 'local7', - }, - { - name: 'syslog.priority', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Syslog numeric priority of the event, if available.\n\nAccording to RFCs 5424 and 3164, the priority is 8 * facility + severity.\nThis number is therefore expected to contain a value between 0 and 191.', - example: 135, - }, - { - name: 'syslog.severity.code', - level: 'extended', - type: 'long', - description: - 'The Syslog numeric severity of the log event, if available.\n\nIf the event source publishing via Syslog provides a different numeric severity\nvalue (e.g. firewall, IDS), your source numeric severity should go to `event.severity`.\nIf the event source does not specify a distinct severity, you can optionally\ncopy the Syslog severity to `event.severity`.', - example: 3, - }, - { - name: 'syslog.severity.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The Syslog numeric severity of the log event, if available.\n\nIf the event source publishing via Syslog provides a different severity value\n(e.g. firewall, IDS), your source text severity should go to `log.level`.\nIf the event source does not specify a distinct severity, you can optionally\ncopy the Syslog severity to `log.level`.', - example: 'Error', - }, - ], - }, - { - name: 'network', - title: 'Network', - group: 2, - description: - 'The network is defined as the communication path over which a host\nor network event happens.\n\nThe network.* fields should be populated with details about the network activity\nassociated with an event.', - type: 'group', - fields: [ - { - name: 'application', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A name given to an application level protocol. This can be arbitrarily\nassigned for things like microservices, but also apply to things like skype,\nicq, facebook, twitter. This would be used in situations where the vendor\nor service can be decoded such as from the source/dest IP owners, ports, or\nwire format.\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'aim', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: - 'Total bytes transferred in both directions.\n\nIf `source.bytes` and `destination.bytes` are known, `network.bytes` is their\nsum.', - example: 368, - }, - { - name: 'community_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A hash of source and destination IPs and ports, as well as the\nprotocol used in a communication. This is a tool-agnostic standard to identify\nflows.\n\nLearn more at https://github.com/corelight/community-id-spec.', - example: '1:hO+sN4H+MG5MY/8hIrXPqc4ZQz0=', - }, - { - name: 'direction', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - "Direction of the network traffic.\nRecommended values are:\n * inbound\n * outbound\n * internal\n * external\n * unknown\n\nWhen mapping events from a host-based monitoring context, populate this field from the host's point of view.\nWhen mapping events from a network or perimeter-based monitoring context, populate this field from the point of view of your network perimeter.", - example: 'inbound', - }, - { - name: 'forwarded_ip', - level: 'core', - type: 'ip', - description: 'Host IP address when the source IP address is the proxy.', - example: '192.1.1.2', - }, - { - name: 'iana_number', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'IANA Protocol Number (https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml).\nStandardized list of protocols. This aligns well with NetFlow and sFlow related\nlogs which use the IANA Protocol Number.', - example: 6, - }, - { - name: 'inner', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'Network.inner fields are added in addition to network.vlan fields\nto describe the innermost VLAN when q-in-q VLAN tagging is present. Allowed\nfields include vlan.id and vlan.name. Inner vlan fields are typically used\nwhen sending traffic with multiple 802.1q encapsulations to a network sensor\n(e.g. Zeek, Wireshark.)', - default_field: false, - }, - { - name: 'inner.vlan.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'inner.vlan.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name given by operators to sections of their network.', - example: 'Guest Wifi', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: - 'Total packets transferred in both directions.\n\nIf `source.packets` and `destination.packets` are known, `network.packets`\nis their sum.', - example: 24, - }, - { - name: 'protocol', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'L7 Network protocol name. ex. http, lumberjack, transport protocol.\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'http', - }, - { - name: 'transport', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Same as network.iana_number, but instead using the Keyword name\nof the transport layer (udp, tcp, ipv6-icmp, etc.)\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'tcp', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'In the OSI Model this would be the Network Layer. ipv4, ipv6,\nipsec, pim, etc\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'ipv4', - }, - { - name: 'vlan.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'vlan.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - ], - }, - { - name: 'observer', - title: 'Observer', - group: 2, - description: - 'An observer is defined as a special network, security, or application\ndevice used to detect, observe, or create network, security, or application-related\nevents and metrics.\n\nThis could be a custom hardware appliance or a server that has been configured\nto run special network, security, or application software. Examples include\nfirewalls, web proxies, intrusion detection/prevention systems, network monitoring\nsensors, web application firewalls, data loss prevention systems, and APM servers.\nThe observer.* fields shall be populated with details of the system, if any,\nthat detects, observes and/or creates a network, security, or application event\nor metric. Message queues and ETL components used in processing events or metrics\nare not considered observers in ECS.', - type: 'group', - fields: [ - { - name: 'egress', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'Observer.egress holds information like interface number and name,\nvlan, and zone information to classify egress traffic. Single armed monitoring\nsuch as a network sensor on a span port should only use observer.ingress\nto categorize traffic.', - default_field: false, - }, - { - name: 'egress.interface.alias', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Interface alias as reported by the system, typically used in firewall\nimplementations for e.g. inside, outside, or dmz logical interface naming.', - example: 'outside', - default_field: false, - }, - { - name: 'egress.interface.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface ID as reported by an observer (typically SNMP interface\nID).', - example: 10, - default_field: false, - }, - { - name: 'egress.interface.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface name as reported by the system.', - example: 'eth0', - default_field: false, - }, - { - name: 'egress.vlan.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'egress.vlan.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - { - name: 'egress.zone', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Network zone of outbound traffic as reported by the observer to\ncategorize the destination area of egress traffic, e.g. Internal, External,\nDMZ, HR, Legal, etc.', - example: 'Public_Internet', - default_field: false, - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'hostname', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Hostname of the observer.', - }, - { - name: 'ingress', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'Observer.ingress holds information like interface number and name,\nvlan, and zone information to classify ingress traffic. Single armed monitoring\nsuch as a network sensor on a span port should only use observer.ingress\nto categorize traffic.', - default_field: false, - }, - { - name: 'ingress.interface.alias', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Interface alias as reported by the system, typically used in firewall\nimplementations for e.g. inside, outside, or dmz logical interface naming.', - example: 'outside', - default_field: false, - }, - { - name: 'ingress.interface.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface ID as reported by an observer (typically SNMP interface\nID).', - example: 10, - default_field: false, - }, - { - name: 'ingress.interface.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface name as reported by the system.', - example: 'eth0', - default_field: false, - }, - { - name: 'ingress.vlan.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'ingress.vlan.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - { - name: 'ingress.zone', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Network zone of incoming traffic as reported by the observer to\ncategorize the source area of ingress traffic. e.g. internal, External, DMZ,\nHR, Legal, etc.', - example: 'DMZ', - default_field: false, - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: 'IP addresses of the observer.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC addresses of the observer', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Custom name of the observer.\n\nThis is a name that can be given to an observer. This can be helpful for example\nif multiple firewalls of the same model are used in an organization.\n\nIf no custom name is needed, the field can be left empty.', - example: '1_proxySG', - }, - { - name: 'os.family', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'os.full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'os.kernel', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'os.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'os.platform', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'os.version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - { - name: 'product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The product name of the observer.', - example: 's200', - }, - { - name: 'serial_number', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Observer serial number.', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of the observer the data is coming from.\n\nThere is no predefined list of observer types. Some examples are `forwarder`,\n`firewall`, `ids`, `ips`, `proxy`, `poller`, `sensor`, `APM server`.', - example: 'firewall', - }, - { - name: 'vendor', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Vendor name of the observer.', - example: 'Symantec', - }, - { - name: 'version', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Observer version.', - }, - ], - }, - { - name: 'organization', - title: 'Organization', - group: 2, - description: - 'The organization fields enrich data with information about the company\nor entity the data is associated with.\n\nThese fields help you arrange or filter data stored in an index by one or multiple\norganizations.', - type: 'group', - fields: [ - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the organization.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - }, - ], - }, - { - name: 'os', - title: 'Operating System', - group: 2, - description: 'The OS fields contain information about the operating system.', - type: 'group', - fields: [ - { - name: 'family', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'kernel', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'platform', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - ], - }, - { - name: 'package', - title: 'Package', - group: 2, - description: - 'These fields contain information about an installed software package.\nIt contains general information about a package, such as name, version or size.\nIt also contains installation details, such as time or location.', - type: 'group', - fields: [ - { - name: 'architecture', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Package architecture.', - example: 'x86_64', - }, - { - name: 'build_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the build version of the installed\npackage.\n\nFor example use the commit SHA of a non-released package.', - example: '36f4f7e89dd61b0988b12ee000b98966867710cd', - default_field: false, - }, - { - name: 'checksum', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Checksum of the installed package for verification.', - example: '68b329da9893e34099c7d8ad5cb9c940', - }, - { - name: 'description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Description of the package.', - example: - 'Open source programming language to build simple/reliable/efficient\nsoftware.', - }, - { - name: 'install_scope', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Indicating how the package was installed, e.g. user-local, global.', - example: 'global', - }, - { - name: 'installed', - level: 'extended', - type: 'date', - description: 'Time when package was installed.', - }, - { - name: 'license', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'License under which the package was released.\n\nUse a short name, e.g. the license identifier from SPDX License List where\npossible (https://spdx.org/licenses/).', - example: 'Apache License 2.0', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Package name', - example: 'go', - }, - { - name: 'path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Path where the package is installed.', - example: '/usr/local/Cellar/go/1.12.9/', - }, - { - name: 'reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Home page or reference URL of the software in this package, if\navailable.', - example: 'https://golang.org', - default_field: false, - }, - { - name: 'size', - level: 'extended', - type: 'long', - format: 'string', - description: 'Package size in bytes.', - example: 62231, - }, - { - name: 'type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Type of package.\n\nThis should contain the package file type, rather than the package manager\nname. Examples: rpm, dpkg, brew, npm, gem, nupkg, jar.', - example: 'rpm', - default_field: false, - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Package version', - example: '1.12.9', - }, - ], - }, - { - name: 'pe', - title: 'PE Header', - group: 2, - description: 'These fields contain Windows Portable Executable (PE) metadata.', - type: 'group', - fields: [ - { - name: 'company', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal company name of the file, provided at compile-time.', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal description of the file, provided at compile-time.', - example: 'Paint', - default_field: false, - }, - { - name: 'file_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal version of the file, provided at compile-time.', - example: '6.3.9600.17415', - default_field: false, - }, - { - name: 'original_file_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal name of the file, provided at compile-time.', - example: 'MSPAINT.EXE', - default_field: false, - }, - { - name: 'product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal product name of the file, provided at compile-time.', - example: 'Microsoft® Windows® Operating System', - default_field: false, - }, - ], - }, - { - name: 'process', - title: 'Process', - group: 2, - description: - 'These fields contain information about a process.\n\nThese fields can help you correlate metrics information with a process id/name\nfrom a log message. The `process.pid` often stays in the metric itself and\nis copied to the global field for correlation.', - type: 'group', - fields: [ - { - name: 'args', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.', - example: ['/usr/bin/ssh', '-l', 'user', '10.0.0.16'], - }, - { - name: 'args_count', - level: 'extended', - type: 'long', - description: - 'Length of the process.args array.\n\nThis field can be useful for querying or performing bucket analysis on how\nmany arguments were provided to start a process. More arguments may be an\nindication of suspicious activity.', - example: 4, - default_field: false, - }, - { - name: 'code_signature.exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'code_signature.subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'code_signature.trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - { - name: 'command_line', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: - 'Full command line that started the process, including the absolute\npath to the executable, and all arguments.\n\nSome arguments may be filtered to protect sensitive information.', - example: '/usr/bin/ssh -l user 10.0.0.16', - default_field: false, - }, - { - name: 'entity_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier for the process.\n\nThe implementation of this is specified by the data source, but some examples\nof what could be used here are a process-generated UUID, Sysmon Process GUIDs,\nor a hash of some uniquely identifying components of a process.\n\nConstructing a globally unique identifier is a common practice to mitigate\nPID reuse as well as to identify a specific process over time, across multiple\nmonitored hosts.', - example: 'c2c455d9f99375d', - default_field: false, - }, - { - name: 'executable', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Absolute path to the process executable.', - example: '/usr/bin/ssh', - }, - { - name: 'exit_code', - level: 'extended', - type: 'long', - description: - 'The exit code of the process, if this is a termination event.\n\nThe field should be absent if there is no exit code for the event (e.g. process\nstart).', - example: 137, - default_field: false, - }, - { - name: 'hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - }, - { - name: 'hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - }, - { - name: 'hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - }, - { - name: 'hash.sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Process name.\n\nSometimes called program name or similar.', - example: 'ssh', - }, - { - name: 'parent.args', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of process arguments.\n\nMay be filtered to protect sensitive information.', - example: ['ssh', '-l', 'user', '10.0.0.16'], - default_field: false, - }, - { - name: 'parent.args_count', - level: 'extended', - type: 'long', - description: - 'Length of the process.args array.\n\nThis field can be useful for querying or performing bucket analysis on how\nmany arguments were provided to start a process. More arguments may be an\nindication of suspicious activity.', - example: 4, - default_field: false, - }, - { - name: 'parent.code_signature.exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'parent.code_signature.status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'parent.code_signature.subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'parent.code_signature.trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'parent.code_signature.valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - { - name: 'parent.command_line', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: - 'Full command line that started the process, including the absolute\npath to the executable, and all arguments.\n\nSome arguments may be filtered to protect sensitive information.', - example: '/usr/bin/ssh -l user 10.0.0.16', - default_field: false, - }, - { - name: 'parent.entity_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier for the process.\n\nThe implementation of this is specified by the data source, but some examples\nof what could be used here are a process-generated UUID, Sysmon Process GUIDs,\nor a hash of some uniquely identifying components of a process.\n\nConstructing a globally unique identifier is a common practice to mitigate\nPID reuse as well as to identify a specific process over time, across multiple\nmonitored hosts.', - example: 'c2c455d9f99375d', - default_field: false, - }, - { - name: 'parent.executable', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: 'Absolute path to the process executable.', - example: '/usr/bin/ssh', - default_field: false, - }, - { - name: 'parent.exit_code', - level: 'extended', - type: 'long', - description: - 'The exit code of the process, if this is a termination event.\n\nThe field should be absent if there is no exit code for the event (e.g. process\nstart).', - example: 137, - default_field: false, - }, - { - name: 'parent.hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - default_field: false, - }, - { - name: 'parent.hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - default_field: false, - }, - { - name: 'parent.hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - default_field: false, - }, - { - name: 'parent.hash.sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - default_field: false, - }, - { - name: 'parent.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: 'Process name.\n\nSometimes called program name or similar.', - example: 'ssh', - default_field: false, - }, - { - name: 'parent.pgid', - level: 'extended', - type: 'long', - format: 'string', - description: 'Identifier of the group of processes the process belongs to.', - default_field: false, - }, - { - name: 'parent.pid', - level: 'core', - type: 'long', - format: 'string', - description: 'Process id.', - example: 4242, - default_field: false, - }, - { - name: 'parent.ppid', - level: 'extended', - type: 'long', - format: 'string', - description: "Parent process' pid.", - example: 4241, - default_field: false, - }, - { - name: 'parent.start', - level: 'extended', - type: 'date', - description: 'The time the process started.', - example: '2016-05-23T08:05:34.853Z', - default_field: false, - }, - { - name: 'parent.thread.id', - level: 'extended', - type: 'long', - format: 'string', - description: 'Thread ID.', - example: 4242, - default_field: false, - }, - { - name: 'parent.thread.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Thread name.', - example: 'thread-0', - default_field: false, - }, - { - name: 'parent.title', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: - 'Process title.\n\nThe proctitle, some times the same as process name. Can also be different:\nfor example a browser setting its title to the web page currently opened.', - default_field: false, - }, - { - name: 'parent.uptime', - level: 'extended', - type: 'long', - description: 'Seconds the process has been up.', - example: 1325, - default_field: false, - }, - { - name: 'parent.working_directory', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: 'The working directory of the process.', - example: '/home/alice', - default_field: false, - }, - { - name: 'pe.company', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal company name of the file, provided at compile-time.', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'pe.description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal description of the file, provided at compile-time.', - example: 'Paint', - default_field: false, - }, - { - name: 'pe.file_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal version of the file, provided at compile-time.', - example: '6.3.9600.17415', - default_field: false, - }, - { - name: 'pe.original_file_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal name of the file, provided at compile-time.', - example: 'MSPAINT.EXE', - default_field: false, - }, - { - name: 'pe.product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal product name of the file, provided at compile-time.', - example: 'Microsoft® Windows® Operating System', - default_field: false, - }, - { - name: 'pgid', - level: 'extended', - type: 'long', - format: 'string', - description: 'Identifier of the group of processes the process belongs to.', - }, - { - name: 'pid', - level: 'core', - type: 'long', - format: 'string', - description: 'Process id.', - example: 4242, - }, - { - name: 'ppid', - level: 'extended', - type: 'long', - format: 'string', - description: "Parent process' pid.", - example: 4241, - }, - { - name: 'start', - level: 'extended', - type: 'date', - description: 'The time the process started.', - example: '2016-05-23T08:05:34.853Z', - }, - { - name: 'thread.id', - level: 'extended', - type: 'long', - format: 'string', - description: 'Thread ID.', - example: 4242, - }, - { - name: 'thread.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Thread name.', - example: 'thread-0', - }, - { - name: 'title', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'Process title.\n\nThe proctitle, some times the same as process name. Can also be different:\nfor example a browser setting its title to the web page currently opened.', - }, - { - name: 'uptime', - level: 'extended', - type: 'long', - description: 'Seconds the process has been up.', - example: 1325, - }, - { - name: 'working_directory', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'The working directory of the process.', - example: '/home/alice', - }, - ], - }, - { - name: 'registry', - title: 'Registry', - group: 2, - description: 'Fields related to Windows Registry operations.', - type: 'group', - fields: [ - { - name: 'data.bytes', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Original bytes written with base64 encoding.\n\nFor Windows registry operations, such as SetValueEx and RegQueryValueEx, this\ncorresponds to the data pointed by `lp_data`. This is optional but provides\nbetter recoverability and should be populated for REG_BINARY encoded values.', - example: 'ZQBuAC0AVQBTAAAAZQBuAAAAAAA=', - default_field: false, - }, - { - name: 'data.strings', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Content when writing string types.\n\nPopulated as an array when writing string data to the registry. For single\nstring registry types (REG_SZ, REG_EXPAND_SZ), this should be an array with\none string. For sequences of string with REG_MULTI_SZ, this array will be\nvariable length. For numeric data, such as REG_DWORD and REG_QWORD, this should\nbe populated with the decimal representation (e.g `"1"`).', - example: '["C:\\rta\\red_ttp\\bin\\myapp.exe"]', - default_field: false, - }, - { - name: 'data.type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Standard registry type for encoding contents', - example: 'REG_SZ', - default_field: false, - }, - { - name: 'hive', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Abbreviated name for the hive.', - example: 'HKLM', - default_field: false, - }, - { - name: 'key', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Hive-relative path of keys.', - example: - 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\winword.exe', - default_field: false, - }, - { - name: 'path', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Full path, including hive, key and value', - example: - 'HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution\nOptions\\winword.exe\\Debugger', - default_field: false, - }, - { - name: 'value', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the value written.', - example: 'Debugger', - default_field: false, - }, - ], - }, - { - name: 'related', - title: 'Related', - group: 2, - description: - 'This field set is meant to facilitate pivoting around a piece of\ndata.\n\nSome pieces of information can be seen in many places in an ECS event. To facilitate\nsearching for them, store an array of all seen values to their corresponding\nfield in `related.`.\n\nA concrete example is IP addresses, which can be under host, observer, source,\ndestination, client, server, and network.forwarded_ip. If you append all IPs\nto `related.ip`, you can then search for a given IP trivially, no matter where\nit appeared, by querying `related.ip:192.0.2.15`.', - type: 'group', - fields: [ - { - name: 'hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - "All the hashes seen on your event. Populating this field, then\nusing it to search for hashes can help in situations where you're unsure what\nthe hash algorithm is (and therefore which key name to search).", - default_field: false, - }, - { - name: 'ip', - level: 'extended', - type: 'ip', - description: 'All of the IPs seen on your event.', - }, - { - name: 'user', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'All the user names seen on your event.', - default_field: false, - }, - ], - }, - { - name: 'rule', - title: 'Rule', - group: 2, - description: - 'Rule fields are used to capture the specifics of any observer or\nagent rules that generate alerts or other notable events.\n\nExamples of data sources that would populate the rule fields include: network\nadmission control platforms, network or host IDS/IPS, network firewalls, web\napplication firewalls, url filters, endpoint detection and response (EDR) systems,\netc.', - type: 'group', - fields: [ - { - name: 'author', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name, organization, or pseudonym of the author or authors who created\nthe rule used to generate this event.', - example: ['Star-Lord'], - default_field: false, - }, - { - name: 'category', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A categorization value keyword used by the entity using the rule\nfor detection of this event.', - example: 'Attempted Information Leak', - default_field: false, - }, - { - name: 'description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The description of the rule generating the event.', - example: 'Block requests to public DNS over HTTPS / TLS protocols', - default_field: false, - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A rule ID that is unique within the scope of an agent, observer,\nor other entity using the rule for detection of this event.', - example: 101, - default_field: false, - }, - { - name: 'license', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the license under which the rule used to generate this\nevent is made available.', - example: 'Apache 2.0', - default_field: false, - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The name of the rule or signature generating the event.', - example: 'BLOCK_DNS_over_TLS', - default_field: false, - }, - { - name: 'reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Reference URL to additional information about the rule used to\ngenerate this event.\n\nThe URL can point to the vendor documentation about the rule. If that is\nnot available, it can also be a link to a more general page describing this\ntype of alert.', - example: 'https://en.wikipedia.org/wiki/DNS_over_TLS', - default_field: false, - }, - { - name: 'ruleset', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the ruleset, policy, group, or parent category in which\nthe rule used to generate this event is a member.', - example: 'Standard_Protocol_Filters', - default_field: false, - }, - { - name: 'uuid', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A rule ID that is unique within the scope of a set or group of\nagents, observers, or other entities using the rule for detection of this\nevent.', - example: 1100110011, - default_field: false, - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The version / revision of the rule being used for analysis.', - example: 1.1, - default_field: false, - }, - ], - }, - { - name: 'server', - title: 'Server', - group: 2, - description: - 'A Server is defined as the responder in a network connection for\nevents regarding sessions, connections, or bidirectional flow records.\n\nFor TCP events, the server is the receiver of the initial SYN packet(s) of the\nTCP connection. For other protocols, the server is generally the responder in\nthe network transaction. Some systems actually use the term "responder" to refer\nthe server in TCP connections. The server fields describe details about the\nsystem acting as the server in the network event. Server fields are usually\npopulated in conjunction with client fields. Server fields are generally not\npopulated for packet-level events.\n\nClient / server representations can add semantic context to an exchange, which\nis helpful to visualize the data in certain situations. If your context falls\nin that category, you should still ensure that source and destination are filled\nappropriately.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Some event server addresses are defined ambiguously. The event\nwill sometimes list an IP, a domain or a unix socket. You should always store\nthe raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'as.number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'as.organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the server to the client.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Server domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: - 'IP address of the server.\n\nCan be one or multiple IPv4 or IPv6 addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC address of the server.', - }, - { - name: 'nat.ip', - level: 'extended', - type: 'ip', - description: - 'Translated ip of destination based NAT sessions (e.g. internet\nto private DMZ)\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'nat.port', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Translated port of destination based NAT sessions (e.g. internet\nto private DMZ)\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the server to the client.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the server.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered server domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'service', - title: 'Service', - group: 2, - description: - 'The service fields describe the service for or from which the data\nwas collected.\n\nThese fields help you find and correlate logs for a specific service and version.', - type: 'group', - fields: [ - { - name: 'ephemeral_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Ephemeral identifier of this service (if one exists).\n\nThis id normally changes across restarts, but `service.id` does not.', - example: '8a4f500f', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier of the running service. If the service is comprised\nof many nodes, the `service.id` should be the same for all nodes.\n\nThis id should uniquely identify the service. This makes it possible to correlate\nlogs and metrics for one specific service, no matter which particular node\nemitted the event.\n\nNote that if you need to see the events from one specific host of the service,\nyou should filter on that `host.name` or `host.id` instead.', - example: 'd37e5ebfe0ae6c4972dbe9f0174a1637bb8247f6', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the service data is collected from.\n\nThe name of the service is normally user given. This allows for distributed\nservices that run on multiple hosts to correlate the related instances based\non the name.\n\nIn the case of Elasticsearch the `service.name` could contain the cluster\nname. For Beats the `service.name` is by default a copy of the `service.type`\nfield if no name is specified.', - example: 'elasticsearch-metrics', - }, - { - name: 'node.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of a service node.\n\nThis allows for two nodes of the same service running on the same host to\nbe differentiated. Therefore, `service.node.name` should typically be unique\nacross nodes of a given service.\n\nIn the case of Elasticsearch, the `service.node.name` could contain the unique\nnode name within the Elasticsearch cluster. In cases where the service does not\nhave the concept of a node name, the host name or container name can be used\nto distinguish running instances that make up this service. If those do not\nprovide uniqueness (e.g. multiple instances of the service running on the\nsame host) - the node name can be manually set.', - example: 'instance-0000000016', - }, - { - name: 'state', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Current state of the service.', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of the service data is collected from.\n\nThe type can be used to group and correlate logs and metrics from one service\ntype.\n\nExample: If logs or metrics are collected from Elasticsearch, `service.type`\nwould be `elasticsearch`.', - example: 'elasticsearch', - }, - { - name: 'version', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Version of the service the data was collected from.\n\nThis allows to look at a data set only for a specific version of a service.', - example: '3.2.4', - }, - ], - }, - { - name: 'source', - title: 'Source', - group: 2, - description: - 'Source fields describe details about the source of a packet/event.\n\nSource fields are usually populated in conjunction with destination fields.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Some event source addresses are defined ambiguously. The event\nwill sometimes list an IP, a domain or a unix socket. You should always store\nthe raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'as.number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'as.organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the source to the destination.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Source domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: - 'IP address of the source.\n\nCan be one or multiple IPv4 or IPv6 addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC address of the source.', - }, - { - name: 'nat.ip', - level: 'extended', - type: 'ip', - description: - 'Translated ip of source based NAT sessions (e.g. internal client\nto internet)\n\nTypically connections traversing load balancers, firewalls, or routers.', - }, - { - name: 'nat.port', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Translated port of source based NAT sessions. (e.g. internal client\nto internet)\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the source to the destination.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the source.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered source domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'threat', - title: 'Threat', - group: 2, - description: - 'Fields to classify events and alerts according to a threat taxonomy\nsuch as the Mitre ATT&CK framework.\n\nThese fields are for users to classify alerts from all of their sources (e.g.\nIDS, NGFW, etc.) within a common taxonomy. The threat.tactic.* are meant to\ncapture the high level category of the threat (e.g. "impact"). The threat.technique.*\nfields are meant to capture which kind of approach is used by this detected\nthreat, to accomplish the goal (e.g. "endpoint denial of service").', - type: 'group', - fields: [ - { - name: 'framework', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the threat framework used to further categorize and classify\nthe tactic and technique of the reported threat. Framework classification\ncan be provided by detecting systems, evaluated at ingest time, or retrospectively\ntagged to events.', - example: 'MITRE ATT&CK', - }, - { - name: 'tactic.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The id of tactic used by this threat. You can use the Mitre ATT&CK\nMatrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/\n)', - example: 'TA0040', - }, - { - name: 'tactic.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the type of tactic used by this threat. You can use the\nMitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/\n)', - example: 'impact', - }, - { - name: 'tactic.reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The reference url of tactic used by this threat. You can use the\nMitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/\n)', - example: 'https://attack.mitre.org/tactics/TA0040/', - }, - { - name: 'technique.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The id of technique used by this tactic. You can use the Mitre\nATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/\n)', - example: 'T1499', - }, - { - name: 'technique.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'The name of technique used by this tactic. You can use the Mitre\nATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/\n)', - example: 'endpoint denial of service', - }, - { - name: 'technique.reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The reference url of technique used by this tactic. You can use\nthe Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/\n)', - example: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - { - name: 'tls', - title: 'TLS', - group: 2, - description: - 'Fields related to a TLS connection. These fields focus on the TLS\nprotocol itself and intentionally avoids in-depth analysis of the related x.509\ncertificate files.', - type: 'group', - fields: [ - { - name: 'cipher', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'String indicating the cipher used during the current connection.', - example: 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256', - default_field: false, - }, - { - name: 'client.certificate', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'PEM-encoded stand-alone certificate offered by the client. This\nis usually mutually-exclusive of `client.certificate_chain` since this value\nalso exists in that list.', - example: 'MII...', - default_field: false, - }, - { - name: 'client.certificate_chain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of PEM-encoded certificates that make up the certificate\nchain offered by the client. This is usually mutually-exclusive of `client.certificate`\nsince that value should be the first certificate in the chain.', - example: ['MII...', 'MII...'], - default_field: false, - }, - { - name: 'client.hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the MD5 digest of DER-encoded version\nof certificate offered by the client. For consistency with other hash values,\nthis value should be formatted as an uppercase hash.', - example: '0F76C7F2C55BFD7D8E8B8F4BFBF0C9EC', - default_field: false, - }, - { - name: 'client.hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the SHA1 digest of DER-encoded version\nof certificate offered by the client. For consistency with other hash values,\nthis value should be formatted as an uppercase hash.', - example: '9E393D93138888D288266C2D915214D1D1CCEB2A', - default_field: false, - }, - { - name: 'client.hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the SHA256 digest of DER-encoded\nversion of certificate offered by the client. For consistency with other hash\nvalues, this value should be formatted as an uppercase hash.', - example: '0687F666A054EF17A08E2F2162EAB4CBC0D265E1D7875BE74BF3C712CA92DAF0', - default_field: false, - }, - { - name: 'client.issuer', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Distinguished name of subject of the issuer of the x.509 certificate\npresented by the client.', - example: 'CN=MyDomain Root CA, OU=Infrastructure Team, DC=mydomain, DC=com', - default_field: false, - }, - { - name: 'client.ja3', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A hash that identifies clients based on how they perform an SSL/TLS\nhandshake.', - example: 'd4e5b18d6b55c71272893221c96ba240', - default_field: false, - }, - { - name: 'client.not_after', - level: 'extended', - type: 'date', - description: - 'Date/Time indicating when client certificate is no longer considered\nvalid.', - example: '2021-01-01T00:00:00.000Z', - default_field: false, - }, - { - name: 'client.not_before', - level: 'extended', - type: 'date', - description: 'Date/Time indicating when client certificate is first considered\nvalid.', - example: '1970-01-01T00:00:00.000Z', - default_field: false, - }, - { - name: 'client.server_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Also called an SNI, this tells the server which hostname to which\nthe client is attempting to connect. When this value is available, it should\nget copied to `destination.domain`.', - example: 'www.elastic.co', - default_field: false, - }, - { - name: 'client.subject', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Distinguished name of subject of the x.509 certificate presented\nby the client.', - example: 'CN=myclient, OU=Documentation Team, DC=mydomain, DC=com', - default_field: false, - }, - { - name: 'client.supported_ciphers', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Array of ciphers offered by the client during the client hello.', - example: [ - 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384', - 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384', - '...', - ], - default_field: false, - }, - { - name: 'curve', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'String indicating the curve used for the given cipher, when applicable.', - example: 'secp256r1', - default_field: false, - }, - { - name: 'established', - level: 'extended', - type: 'boolean', - description: - 'Boolean flag indicating if the TLS negotiation was successful and\ntransitioned to an encrypted tunnel.', - default_field: false, - }, - { - name: 'next_protocol', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'String indicating the protocol being tunneled. Per the values in\nthe IANA registry (https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids),\nthis string should be lower case.', - example: 'http/1.1', - default_field: false, - }, - { - name: 'resumed', - level: 'extended', - type: 'boolean', - description: - 'Boolean flag indicating if this TLS connection was resumed from\nan existing TLS negotiation.', - default_field: false, - }, - { - name: 'server.certificate', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'PEM-encoded stand-alone certificate offered by the server. This\nis usually mutually-exclusive of `server.certificate_chain` since this value\nalso exists in that list.', - example: 'MII...', - default_field: false, - }, - { - name: 'server.certificate_chain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of PEM-encoded certificates that make up the certificate\nchain offered by the server. This is usually mutually-exclusive of `server.certificate`\nsince that value should be the first certificate in the chain.', - example: ['MII...', 'MII...'], - default_field: false, - }, - { - name: 'server.hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the MD5 digest of DER-encoded version\nof certificate offered by the server. For consistency with other hash values,\nthis value should be formatted as an uppercase hash.', - example: '0F76C7F2C55BFD7D8E8B8F4BFBF0C9EC', - default_field: false, - }, - { - name: 'server.hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the SHA1 digest of DER-encoded version\nof certificate offered by the server. For consistency with other hash values,\nthis value should be formatted as an uppercase hash.', - example: '9E393D93138888D288266C2D915214D1D1CCEB2A', - default_field: false, - }, - { - name: 'server.hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the SHA256 digest of DER-encoded\nversion of certificate offered by the server. For consistency with other hash\nvalues, this value should be formatted as an uppercase hash.', - example: '0687F666A054EF17A08E2F2162EAB4CBC0D265E1D7875BE74BF3C712CA92DAF0', - default_field: false, - }, - { - name: 'server.issuer', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Subject of the issuer of the x.509 certificate presented by the\nserver.', - example: 'CN=MyDomain Root CA, OU=Infrastructure Team, DC=mydomain, DC=com', - default_field: false, - }, - { - name: 'server.ja3s', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A hash that identifies servers based on how they perform an SSL/TLS\nhandshake.', - example: '394441ab65754e2207b1e1b457b3641d', - default_field: false, - }, - { - name: 'server.not_after', - level: 'extended', - type: 'date', - description: - 'Timestamp indicating when server certificate is no longer considered\nvalid.', - example: '2021-01-01T00:00:00.000Z', - default_field: false, - }, - { - name: 'server.not_before', - level: 'extended', - type: 'date', - description: 'Timestamp indicating when server certificate is first considered\nvalid.', - example: '1970-01-01T00:00:00.000Z', - default_field: false, - }, - { - name: 'server.subject', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Subject of the x.509 certificate presented by the server.', - example: 'CN=www.mydomain.com, OU=Infrastructure Team, DC=mydomain, DC=com', - default_field: false, - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Numeric part of the version parsed from the original string.', - example: '1.2', - default_field: false, - }, - { - name: 'version_protocol', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Normalized lowercase protocol name parsed from original string.', - example: 'tls', - default_field: false, - }, - ], - }, - { - name: 'tracing', - title: 'Tracing', - group: 2, - description: - 'Distributed tracing makes it possible to analyze performance throughout\na microservice architecture all in one view. This is accomplished by tracing\nall of the requests - from the initial web request in the front-end service\n- to queries made through multiple back-end services.', - type: 'group', - fields: [ - { - name: 'trace.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier of the trace.\n\nA trace groups multiple events like transactions that belong together. For\nexample, a user request handled by multiple inter-connected services.', - example: '4bf92f3577b34da6a3ce929d0e0e4736', - }, - { - name: 'transaction.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier of the transaction.\n\nA transaction is the highest level of work measured within a service, such\nas a request to a server.', - example: '00f067aa0ba902b7', - }, - ], - }, - { - name: 'url', - title: 'URL', - group: 2, - description: - 'URL fields provide support for complete or partial URLs, and supports\nthe breaking down into scheme, domain, path, and so on.', - type: 'group', - fields: [ - { - name: 'domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Domain of the url, such as "www.elastic.co".\n\nIn some cases a URL may refer to an IP and/or port directly, without a domain\nname. In this case, the IP address would go to the `domain` field.', - example: 'www.elastic.co', - }, - { - name: 'extension', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The field contains the file extension from the original request\nurl.\n\nThe file extension is only set if it exists, as not every url has a file extension.\n\nThe leading period must not be included. For example, the value must be "png",\nnot ".png".', - example: 'png', - }, - { - name: 'fragment', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Portion of the url after the `#`, such as "top".\n\nThe `#` is not part of the fragment.', - }, - { - name: 'full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'If full URLs are important to your use case, they should be stored\nin `url.full`, whether this field is reconstructed or present in the event\nsource.', - example: 'https://www.elastic.co:443/search?q=elasticsearch#top', - }, - { - name: 'original', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'Unmodified original url as seen in the event source.\n\nNote that in network monitoring, the observed URL may be a full URL, whereas\nin access logs, the URL is often just represented as a path.\n\nThis field is meant to represent the URL as it was observed, complete or not.', - example: - 'https://www.elastic.co:443/search?q=elasticsearch#top or /search?q=elasticsearch', - }, - { - name: 'password', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Password of the request.', - }, - { - name: 'path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Path of the request, such as "/search".', - }, - { - name: 'port', - level: 'extended', - type: 'long', - format: 'string', - description: 'Port of the request, such as 443.', - example: 443, - }, - { - name: 'query', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The query field describes the query string of the request, such\nas "q=elasticsearch".\n\nThe `?` is excluded from the query string. If a URL contains no `?`, there\nis no query field. If there is a `?` but no query, the query field exists\nwith an empty string. The `exists` query can be used to differentiate between\nthe two cases.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered url domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'scheme', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Scheme of the request, such as "https".\n\nNote: The `:` is not part of the scheme.', - example: 'https', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'username', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Username of the request.', - }, - ], - }, - { - name: 'user', - title: 'User', - group: 2, - description: - 'The user fields describe information about the user that is relevant\nto the event.\n\nFields can have one entry or multiple entries. If a user has more than one id,\nprovide an array that includes all of them.', - type: 'group', - fields: [ - { - name: 'domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifiers of the user.', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'user_agent', - title: 'User agent', - group: 2, - description: - 'The user_agent fields normally come from a browser request.\n\nThey often show up in web service logs coming from the parsed user agent string.', - type: 'group', - fields: [ - { - name: 'device.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the device.', - example: 'iPhone', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the user agent.', - example: 'Safari', - }, - { - name: 'original', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: 'Unparsed user_agent string.', - example: - 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_1 like Mac OS X) AppleWebKit/605.1.15\n(KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1', - }, - { - name: 'os.family', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'os.full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'os.kernel', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'os.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'os.platform', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'os.version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Version of the user agent.', - example: 12, - }, - ], - }, - { - name: 'vlan', - title: 'VLAN', - group: 2, - description: - 'The VLAN fields are used to identify 802.1q tag(s) of a packet,\nas well as ingress and egress VLAN associations of an observer in relation to\na specific packet or connection.\n\nNetwork.vlan fields are used to record a single VLAN tag, or the outer tag in\nthe case of q-in-q encapsulations, for a packet or connection as observed, typically\nprovided by a network sensor (e.g. Zeek, Wireshark) passively reporting on traffic.\n\nNetwork.inner VLAN fields are used to report inner q-in-q 802.1q tags (multiple\n802.1q encapsulations) as observed, typically provided by a network sensor (e.g.\nZeek, Wireshark) passively reporting on traffic. Network.inner VLAN fields should\nonly be used in addition to network.vlan fields to indicate q-in-q tagging.\n\nObserver.ingress and observer.egress VLAN values are used to record observer\nspecific information when observer events contain discrete ingress and egress\nVLAN information, typically provided by firewalls, routers, or load balancers.', - type: 'group', - fields: [ - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - ], - }, - { - name: 'vulnerability', - title: 'Vulnerability', - group: 2, - description: - 'The vulnerability fields describe information about a vulnerability\nthat is relevant to an event.', - type: 'group', - fields: [ - { - name: 'category', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of system or architecture that the vulnerability affects.\nThese may be platform-specific (for example, Debian or SUSE) or general (for\nexample, Database or Firewall). For example (https://qualysguard.qualys.com/qwebhelp/fo_portal/knowledgebase/vulnerability_categories.htm[Qualys\nvulnerability categories])\n\nThis field must be an array.', - example: '["Firewall"]', - default_field: false, - }, - { - name: 'classification', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The classification of the vulnerability scoring system. For example\n(https://www.first.org/cvss/)', - example: 'CVSS', - default_field: false, - }, - { - name: 'description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: - 'The description of the vulnerability that provides additional context\nof the vulnerability. For example (https://cve.mitre.org/about/faqs.html#cve_entry_descriptions_created[Common\nVulnerabilities and Exposure CVE description])', - example: 'In macOS before 2.12.6, there is a vulnerability in the RPC...', - default_field: false, - }, - { - name: 'enumeration', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of identifier used for this vulnerability. For example\n(https://cve.mitre.org/about/)', - example: 'CVE', - default_field: false, - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The identification (ID) is the number portion of a vulnerability\nentry. It includes a unique identification number for the vulnerability. For\nexample (https://cve.mitre.org/about/faqs.html#what_is_cve_id)[Common Vulnerabilities\nand Exposure CVE ID]', - example: 'CVE-2019-00001', - default_field: false, - }, - { - name: 'reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A resource that provides additional information, context, and mitigations\nfor the identified vulnerability.', - example: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-6111', - default_field: false, - }, - { - name: 'report_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The report or scan identification number.', - example: 20191018.0001, - default_field: false, - }, - { - name: 'scanner.vendor', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The name of the vulnerability scanner vendor.', - example: 'Tenable', - default_field: false, - }, - { - name: 'score.base', - level: 'extended', - type: 'float', - description: - 'Scores can range from 0.0 to 10.0, with 10.0 being the most severe.\n\nBase scores cover an assessment for exploitability metrics (attack vector,\ncomplexity, privileges, and user interaction), impact metrics (confidentiality,\nintegrity, and availability), and scope. For example (https://www.first.org/cvss/specification-document)', - example: 5.5, - default_field: false, - }, - { - name: 'score.environmental', - level: 'extended', - type: 'float', - description: - 'Scores can range from 0.0 to 10.0, with 10.0 being the most severe.\n\nEnvironmental scores cover an assessment for any modified Base metrics, confidentiality,\nintegrity, and availability requirements. For example (https://www.first.org/cvss/specification-document)', - example: 5.5, - default_field: false, - }, - { - name: 'score.temporal', - level: 'extended', - type: 'float', - description: - 'Scores can range from 0.0 to 10.0, with 10.0 being the most severe.\n\nTemporal scores cover an assessment for code maturity, remediation level,\nand confidence. For example (https://www.first.org/cvss/specification-document)', - default_field: false, - }, - { - name: 'score.version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The National Vulnerability Database (NVD) provides qualitative\nseverity rankings of "Low", "Medium", and "High" for CVSS v2.0 base score\nranges in addition to the severity ratings for CVSS v3.0 as they are defined\nin the CVSS v3.0 specification.\n\nCVSS is owned and managed by FIRST.Org, Inc. (FIRST), a US-based non-profit\norganization, whose mission is to help computer security incident response\nteams across the world. For example (https://nvd.nist.gov/vuln-metrics/cvss)', - example: 2, - default_field: false, - }, - { - name: 'severity', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The severity of the vulnerability can help with metrics and internal\nprioritization regarding remediation. For example (https://nvd.nist.gov/vuln-metrics/cvss)', - example: 'Critical', - default_field: false, - }, - ], - }, - ], - }, - { - key: 'beat', - anchor: 'beat-common', - title: 'Beat', - description: 'Contains common beat fields available in all event types.\n', - fields: [ - { - name: 'agent.hostname', - type: 'keyword', - description: 'Hostname of the agent.', - }, - { - name: 'beat.timezone', - type: 'alias', - path: 'event.timezone', - migration: true, - }, - { - name: 'fields', - type: 'object', - object_type: 'keyword', - description: 'Contains user configurable fields.\n', - }, - { - name: 'beat.name', - type: 'alias', - path: 'host.name', - migration: true, - }, - { - name: 'beat.hostname', - type: 'alias', - path: 'agent.hostname', - migration: true, - }, - { - name: 'timeseries.instance', - type: 'keyword', - description: 'Time series instance id', - }, - ], - }, - { - key: 'cloud', - title: 'Cloud provider metadata', - description: 'Metadata from cloud providers added by the add_cloud_metadata processor.\n', - fields: [ - { - name: 'cloud.project.id', - example: 'project-x', - description: 'Name of the project in Google Cloud.\n', - }, - { - name: 'cloud.image.id', - example: 'ami-abcd1234', - description: 'Image ID for the cloud instance.\n', - }, - { - name: 'meta.cloud.provider', - type: 'alias', - path: 'cloud.provider', - migration: true, - }, - { - name: 'meta.cloud.instance_id', - type: 'alias', - path: 'cloud.instance.id', - migration: true, - }, - { - name: 'meta.cloud.instance_name', - type: 'alias', - path: 'cloud.instance.name', - migration: true, - }, - { - name: 'meta.cloud.machine_type', - type: 'alias', - path: 'cloud.machine.type', - migration: true, - }, - { - name: 'meta.cloud.availability_zone', - type: 'alias', - path: 'cloud.availability_zone', - migration: true, - }, - { - name: 'meta.cloud.project_id', - type: 'alias', - path: 'cloud.project.id', - migration: true, - }, - { - name: 'meta.cloud.region', - type: 'alias', - path: 'cloud.region', - migration: true, - }, - ], - }, - { - key: 'docker', - title: 'Docker', - description: 'Docker stats collected from Docker.\n', - short_config: false, - anchor: 'docker-processor', - fields: [ - { - name: 'docker', - type: 'group', - fields: [ - { - name: 'container.id', - type: 'alias', - path: 'container.id', - migration: true, - }, - { - name: 'container.image', - type: 'alias', - path: 'container.image.name', - migration: true, - }, - { - name: 'container.name', - type: 'alias', - path: 'container.name', - migration: true, - }, - { - name: 'container.labels', - type: 'object', - object_type: 'keyword', - description: 'Image labels.\n', - }, - ], - }, - ], - }, - { - key: 'host', - title: 'Host', - description: 'Info collected for the host machine.\n', - anchor: 'host-processor', - fields: [ - { - name: 'host', - type: 'group', - fields: [ - { - name: 'containerized', - type: 'boolean', - description: 'If the host is a container.\n', - }, - { - name: 'os.build', - type: 'keyword', - example: '18D109', - description: 'OS build information.\n', - }, - { - name: 'os.codename', - type: 'keyword', - example: 'stretch', - description: 'OS codename, if any.\n', - }, - ], - }, - ], - }, - { - key: 'kubernetes', - title: 'Kubernetes', - description: 'Kubernetes metadata added by the kubernetes processor\n', - short_config: false, - anchor: 'kubernetes-processor', - fields: [ - { - name: 'kubernetes', - type: 'group', - fields: [ - { - name: 'pod.name', - type: 'keyword', - description: 'Kubernetes pod name\n', - }, - { - name: 'pod.uid', - type: 'keyword', - description: 'Kubernetes Pod UID\n', - }, - { - name: 'namespace', - type: 'keyword', - description: 'Kubernetes namespace\n', - }, - { - name: 'node.name', - type: 'keyword', - description: 'Kubernetes node name\n', - }, - { - name: 'labels.*', - type: 'object', - object_type: 'keyword', - object_type_mapping_type: '*', - description: 'Kubernetes labels map\n', - }, - { - name: 'annotations.*', - type: 'object', - object_type: 'keyword', - object_type_mapping_type: '*', - description: 'Kubernetes annotations map\n', - }, - { - name: 'replicaset.name', - type: 'keyword', - description: 'Kubernetes replicaset name\n', - }, - { - name: 'deployment.name', - type: 'keyword', - description: 'Kubernetes deployment name\n', - }, - { - name: 'statefulset.name', - type: 'keyword', - description: 'Kubernetes statefulset name\n', - }, - { - name: 'container.name', - type: 'keyword', - description: 'Kubernetes container name\n', - }, - { - name: 'container.image', - type: 'keyword', - description: 'Kubernetes container image\n', - }, - ], - }, - ], - }, - { - key: 'process', - title: 'Process', - description: 'Process metadata fields\n', - fields: [ - { - name: 'process', - type: 'group', - fields: [ - { - name: 'exe', - type: 'alias', - path: 'process.executable', - migration: true, - }, - ], - }, - ], - }, - { - key: 'jolokia-autodiscover', - title: 'Jolokia Discovery autodiscover provider', - description: 'Metadata from Jolokia Discovery added by the jolokia provider.\n', - fields: [ - { - name: 'jolokia.agent.version', - type: 'keyword', - description: 'Version number of jolokia agent.\n', - }, - { - name: 'jolokia.agent.id', - type: 'keyword', - description: - 'Each agent has a unique id which can be either provided during startup of the agent in form of a configuration parameter or being autodetected. If autodected, the id has several parts: The IP, the process id, hashcode of the agent and its type.\n', - }, - { - name: 'jolokia.server.product', - type: 'keyword', - description: 'The container product if detected.\n', - }, - { - name: 'jolokia.server.version', - type: 'keyword', - description: "The container's version (if detected).\n", - }, - { - name: 'jolokia.server.vendor', - type: 'keyword', - description: 'The vendor of the container the agent is running in.\n', - }, - { - name: 'jolokia.url', - type: 'keyword', - description: 'The URL how this agent can be contacted.\n', - }, - { - name: 'jolokia.secured', - type: 'boolean', - description: 'Whether the agent was configured for authentication or not.\n', - }, - ], - }, - { - key: 'common', - title: 'Common', - description: 'Contains common fields available in all event types.\n', - fields: [ - { - name: 'file', - type: 'group', - description: 'File attributes.', - fields: [ - { - name: 'setuid', - type: 'boolean', - example: true, - description: 'Set if the file has the `setuid` bit set. Omitted otherwise.', - }, - { - name: 'setgid', - type: 'boolean', - example: true, - description: 'Set if the file has the `setgid` bit set. Omitted otherwise.', - }, - { - name: 'origin', - type: 'keyword', - description: - 'An array of strings describing a possible external origin for this file. For example, the URL it was downloaded from. Only supported in macOS, via the kMDItemWhereFroms attribute. Omitted if origin information is not available.\n', - multi_fields: [ - { - name: 'raw', - type: 'keyword', - description: - 'This is a non-analyzed field that is useful for aggregations on the origin data.\n', - }, - ], - }, - { - name: 'selinux', - type: 'group', - description: 'The SELinux identity of the file.', - fields: [ - { - name: 'user', - type: 'keyword', - description: 'The owner of the object.', - }, - { - name: 'role', - type: 'keyword', - description: "The object's SELinux role.", - }, - { - name: 'domain', - type: 'keyword', - description: "The object's SELinux domain or type.", - }, - { - name: 'level', - type: 'keyword', - example: 's0', - description: "The object's SELinux level.", - }, - ], - }, - ], - }, - { - name: 'user', - type: 'group', - description: 'User information.', - fields: [ - { - name: 'audit', - type: 'group', - description: 'Audit user information.', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'Audit user ID.', - }, - { - name: 'name', - type: 'keyword', - description: 'Audit user name.', - }, - ], - }, - { - name: 'effective', - type: 'group', - description: 'Effective user information.', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'Effective user ID.', - }, - { - name: 'name', - type: 'keyword', - description: 'Effective user name.', - }, - { - name: 'group', - type: 'group', - description: 'Effective group information.', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'Effective group ID.', - }, - { - name: 'name', - type: 'keyword', - description: 'Effective group name.', - }, - ], - }, - ], - }, - { - name: 'filesystem', - type: 'group', - description: 'Filesystem user information.', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'Filesystem user ID.', - }, - { - name: 'name', - type: 'keyword', - description: 'Filesystem user name.', - }, - { - name: 'group', - type: 'group', - description: 'Filesystem group information.', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'Filesystem group ID.', - }, - { - name: 'name', - type: 'keyword', - description: 'Filesystem group name.', - }, - ], - }, - ], - }, - { - name: 'saved', - type: 'group', - description: 'Saved user information.', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'Saved user ID.', - }, - { - name: 'name', - type: 'keyword', - description: 'Saved user name.', - }, - { - name: 'group', - type: 'group', - description: 'Saved group information.', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'Saved group ID.', - }, - { - name: 'name', - type: 'keyword', - description: 'Saved group name.', - }, - ], - }, - ], - }, - ], - }, - ], - }, - { - key: 'auditd', - title: 'Auditd', - description: 'These are the fields generated by the auditd module.', - fields: [ - { - name: 'user', - type: 'group', - fields: [ - { - name: 'auid', - type: 'alias', - path: 'user.audit.id', - migration: true, - }, - { - name: 'uid', - type: 'alias', - path: 'user.id', - migration: true, - }, - { - name: 'euid', - type: 'alias', - path: 'user.effective.id', - migration: true, - }, - { - name: 'fsuid', - type: 'alias', - path: 'user.filesystem.id', - migration: true, - }, - { - name: 'suid', - type: 'alias', - path: 'user.saved.id', - migration: true, - }, - { - name: 'gid', - type: 'alias', - path: 'user.group.id', - migration: true, - }, - { - name: 'egid', - type: 'alias', - path: 'user.effective.group.id', - migration: true, - }, - { - name: 'sgid', - type: 'alias', - path: 'user.saved.group.id', - migration: true, - }, - { - name: 'fsgid', - type: 'alias', - path: 'user.filesystem.group.id', - migration: true, - }, - { - name: 'name_map', - type: 'group', - description: - 'If `resolve_ids` is set to true in the configuration then `name_map` will contain a mapping of uid field names to the resolved name (e.g. auid -> root).\n', - fields: [ - { - name: 'auid', - type: 'alias', - path: 'user.audit.name', - migration: true, - }, - { - name: 'uid', - type: 'alias', - path: 'user.name', - migration: true, - }, - { - name: 'euid', - type: 'alias', - path: 'user.effective.name', - migration: true, - }, - { - name: 'fsuid', - type: 'alias', - path: 'user.filesystem.name', - migration: true, - }, - { - name: 'suid', - type: 'alias', - path: 'user.saved.name', - migration: true, - }, - { - name: 'gid', - type: 'alias', - path: 'user.group.name', - migration: true, - }, - { - name: 'egid', - type: 'alias', - path: 'user.effective.group.name', - migration: true, - }, - { - name: 'sgid', - type: 'alias', - path: 'user.saved.group.name', - migration: true, - }, - { - name: 'fsgid', - type: 'alias', - path: 'user.filesystem.group.name', - migration: true, - }, - ], - }, - { - name: 'selinux', - type: 'group', - description: 'The SELinux identity of the actor.', - fields: [ - { - name: 'user', - type: 'keyword', - description: 'account submitted for authentication', - }, - { - name: 'role', - type: 'keyword', - description: "user's SELinux role", - }, - { - name: 'domain', - type: 'keyword', - description: "The actor's SELinux domain or type.", - }, - { - name: 'level', - type: 'keyword', - example: 's0', - description: "The actor's SELinux level.", - }, - { - name: 'category', - type: 'keyword', - description: "The actor's SELinux category or compartments.", - }, - ], - }, - ], - }, - { - name: 'process', - type: 'group', - description: 'Process attributes.', - fields: [ - { - name: 'cwd', - type: 'alias', - path: 'process.working_directory', - migration: true, - description: 'The current working directory.', - }, - ], - }, - { - name: 'source', - type: 'group', - description: 'Source that triggered the event.', - fields: [ - { - name: 'path', - type: 'keyword', - description: 'This is the path associated with a unix socket.', - }, - ], - }, - { - name: 'destination', - type: 'group', - description: 'Destination address that triggered the event.', - fields: [ - { - name: 'path', - type: 'keyword', - description: 'This is the path associated with a unix socket.', - }, - ], - }, - { - name: 'auditd', - type: 'group', - fields: [ - { - name: 'message_type', - type: 'keyword', - example: 'syscall', - description: 'The audit message type (e.g. syscall or apparmor_denied).\n', - }, - { - name: 'sequence', - type: 'long', - description: - 'The sequence number of the event as assigned by the kernel. Sequence numbers are stored as a uint32 in the kernel and can rollover.\n', - }, - { - name: 'session', - type: 'keyword', - description: - 'The session ID assigned to a login. All events related to a login session will have the same value.\n', - }, - { - name: 'result', - type: 'keyword', - example: 'success or fail', - description: 'The result of the audited operation (success/fail).', - }, - { - name: 'summary', - type: 'group', - fields: [ - { - name: 'actor', - type: 'group', - description: 'The actor is the user that triggered the audit event.', - fields: [ - { - name: 'primary', - type: 'keyword', - description: - "The primary identity of the actor. This is the actor's original login ID. It will not change even if the user changes to another account.\n", - }, - { - name: 'secondary', - type: 'keyword', - description: - 'The secondary identity of the actor. This is typically\nthe same as the primary, except for when the user has used `su`.', - }, - ], - }, - { - name: 'object', - type: 'group', - description: 'This is the thing or object being acted upon in the event.\n', - fields: [ - { - name: 'type', - type: 'keyword', - description: - 'A description of the what the "thing" is (e.g. file, socket, user-session).\n', - }, - { - name: 'primary', - type: 'keyword', - description: '', - }, - { - name: 'secondary', - type: 'keyword', - description: '', - }, - ], - }, - { - name: 'how', - type: 'keyword', - description: - 'This describes how the action was performed. Usually this is the exe or command that was being executed that triggered the event.\n', - }, - ], - }, - { - name: 'paths', - type: 'group', - description: 'List of paths associated with the event.', - fields: [ - { - name: 'inode', - type: 'keyword', - description: 'inode number', - }, - { - name: 'dev', - type: 'keyword', - description: 'device name as found in /dev', - }, - { - name: 'obj_user', - type: 'keyword', - description: '', - }, - { - name: 'obj_role', - type: 'keyword', - description: '', - }, - { - name: 'obj_domain', - type: 'keyword', - description: '', - }, - { - name: 'obj_level', - type: 'keyword', - description: '', - }, - { - name: 'objtype', - type: 'keyword', - description: '', - }, - { - name: 'ouid', - type: 'keyword', - description: 'file owner user ID', - }, - { - name: 'rdev', - type: 'keyword', - description: 'the device identifier (special files only)', - }, - { - name: 'nametype', - type: 'keyword', - description: 'kind of file operation being referenced', - }, - { - name: 'ogid', - type: 'keyword', - description: 'file owner group ID', - }, - { - name: 'item', - type: 'keyword', - description: 'which item is being recorded', - }, - { - name: 'mode', - type: 'keyword', - description: 'mode flags on a file', - }, - { - name: 'name', - type: 'keyword', - description: 'file name in avcs', - }, - ], - }, - { - name: 'data', - type: 'group', - description: 'The data from the audit messages.', - fields: [ - { - name: 'action', - type: 'keyword', - description: 'netfilter packet disposition', - }, - { - name: 'minor', - type: 'keyword', - description: 'device minor number', - }, - { - name: 'acct', - type: 'keyword', - description: "a user's account name", - }, - { - name: 'addr', - type: 'keyword', - description: 'the remote address that the user is connecting from', - }, - { - name: 'cipher', - type: 'keyword', - description: 'name of crypto cipher selected', - }, - { - name: 'id', - type: 'keyword', - description: 'during account changes', - }, - { - name: 'entries', - type: 'keyword', - description: 'number of entries in the netfilter table', - }, - { - name: 'kind', - type: 'keyword', - description: 'server or client in crypto operation', - }, - { - name: 'ksize', - type: 'keyword', - description: 'key size for crypto operation', - }, - { - name: 'spid', - type: 'keyword', - description: 'sent process ID', - }, - { - name: 'arch', - type: 'keyword', - description: 'the elf architecture flags', - }, - { - name: 'argc', - type: 'keyword', - description: 'the number of arguments to an execve syscall', - }, - { - name: 'major', - type: 'keyword', - description: 'device major number', - }, - { - name: 'unit', - type: 'keyword', - description: 'systemd unit', - }, - { - name: 'table', - type: 'keyword', - description: 'netfilter table name', - }, - { - name: 'terminal', - type: 'keyword', - description: 'terminal name the user is running programs on', - }, - { - name: 'grantors', - type: 'keyword', - description: 'pam modules approving the action', - }, - { - name: 'direction', - type: 'keyword', - description: 'direction of crypto operation', - }, - { - name: 'op', - type: 'keyword', - description: 'the operation being performed that is audited', - }, - { - name: 'tty', - type: 'keyword', - description: 'tty udevice the user is running programs on', - }, - { - name: 'syscall', - type: 'keyword', - description: 'syscall number in effect when the event occurred', - }, - { - name: 'data', - type: 'keyword', - description: 'TTY text', - }, - { - name: 'family', - type: 'keyword', - description: 'netfilter protocol', - }, - { - name: 'mac', - type: 'keyword', - description: 'crypto MAC algorithm selected', - }, - { - name: 'pfs', - type: 'keyword', - description: 'perfect forward secrecy method', - }, - { - name: 'items', - type: 'keyword', - description: 'the number of path records in the event', - }, - { - name: 'a0', - type: 'keyword', - description: '', - }, - { - name: 'a1', - type: 'keyword', - description: '', - }, - { - name: 'a2', - type: 'keyword', - description: '', - }, - { - name: 'a3', - type: 'keyword', - description: '', - }, - { - name: 'hostname', - type: 'keyword', - description: 'the hostname that the user is connecting from', - }, - { - name: 'lport', - type: 'keyword', - description: 'local network port', - }, - { - name: 'rport', - type: 'keyword', - description: 'remote port number', - }, - { - name: 'exit', - type: 'keyword', - description: 'syscall exit code', - }, - { - name: 'fp', - type: 'keyword', - description: 'crypto key finger print', - }, - { - name: 'laddr', - type: 'keyword', - description: 'local network address', - }, - { - name: 'sport', - type: 'keyword', - description: 'local port number', - }, - { - name: 'capability', - type: 'keyword', - description: 'posix capabilities', - }, - { - name: 'nargs', - type: 'keyword', - description: 'the number of arguments to a socket call', - }, - { - name: 'new-enabled', - type: 'keyword', - description: 'new TTY audit enabled setting', - }, - { - name: 'audit_backlog_limit', - type: 'keyword', - description: "audit system's backlog queue size", - }, - { - name: 'dir', - type: 'keyword', - description: 'directory name', - }, - { - name: 'cap_pe', - type: 'keyword', - description: 'process effective capability map', - }, - { - name: 'model', - type: 'keyword', - description: 'security model being used for virt', - }, - { - name: 'new_pp', - type: 'keyword', - description: 'new process permitted capability map', - }, - { - name: 'old-enabled', - type: 'keyword', - description: 'present TTY audit enabled setting', - }, - { - name: 'oauid', - type: 'keyword', - description: "object's login user ID", - }, - { - name: 'old', - type: 'keyword', - description: 'old value', - }, - { - name: 'banners', - type: 'keyword', - description: 'banners used on printed page', - }, - { - name: 'feature', - type: 'keyword', - description: 'kernel feature being changed', - }, - { - name: 'vm-ctx', - type: 'keyword', - description: "the vm's context string", - }, - { - name: 'opid', - type: 'keyword', - description: "object's process ID", - }, - { - name: 'seperms', - type: 'keyword', - description: 'SELinux permissions being used', - }, - { - name: 'seresult', - type: 'keyword', - description: 'SELinux AVC decision granted/denied', - }, - { - name: 'new-rng', - type: 'keyword', - description: 'device name of rng being added from a vm', - }, - { - name: 'old-net', - type: 'keyword', - description: 'present MAC address assigned to vm', - }, - { - name: 'sigev_signo', - type: 'keyword', - description: 'signal number', - }, - { - name: 'ino', - type: 'keyword', - description: 'inode number', - }, - { - name: 'old_enforcing', - type: 'keyword', - description: 'old MAC enforcement status', - }, - { - name: 'old-vcpu', - type: 'keyword', - description: 'present number of CPU cores', - }, - { - name: 'range', - type: 'keyword', - description: "user's SE Linux range", - }, - { - name: 'res', - type: 'keyword', - description: 'result of the audited operation(success/fail)', - }, - { - name: 'added', - type: 'keyword', - description: 'number of new files detected', - }, - { - name: 'fam', - type: 'keyword', - description: 'socket address family', - }, - { - name: 'nlnk-pid', - type: 'keyword', - description: 'pid of netlink packet sender', - }, - { - name: 'subj', - type: 'keyword', - description: "lspp subject's context string", - }, - { - name: 'a[0-3]', - type: 'keyword', - description: 'the arguments to a syscall', - }, - { - name: 'cgroup', - type: 'keyword', - description: 'path to cgroup in sysfs', - }, - { - name: 'kernel', - type: 'keyword', - description: "kernel's version number", - }, - { - name: 'ocomm', - type: 'keyword', - description: "object's command line name", - }, - { - name: 'new-net', - type: 'keyword', - description: 'MAC address being assigned to vm', - }, - { - name: 'permissive', - type: 'keyword', - description: 'SELinux is in permissive mode', - }, - { - name: 'class', - type: 'keyword', - description: 'resource class assigned to vm', - }, - { - name: 'compat', - type: 'keyword', - description: 'is_compat_task result', - }, - { - name: 'fi', - type: 'keyword', - description: 'file assigned inherited capability map', - }, - { - name: 'changed', - type: 'keyword', - description: 'number of changed files', - }, - { - name: 'msg', - type: 'keyword', - description: 'the payload of the audit record', - }, - { - name: 'dport', - type: 'keyword', - description: 'remote port number', - }, - { - name: 'new-seuser', - type: 'keyword', - description: 'new SELinux user', - }, - { - name: 'invalid_context', - type: 'keyword', - description: 'SELinux context', - }, - { - name: 'dmac', - type: 'keyword', - description: 'remote MAC address', - }, - { - name: 'ipx-net', - type: 'keyword', - description: 'IPX network number', - }, - { - name: 'iuid', - type: 'keyword', - description: "ipc object's user ID", - }, - { - name: 'macproto', - type: 'keyword', - description: 'ethernet packet type ID field', - }, - { - name: 'obj', - type: 'keyword', - description: 'lspp object context string', - }, - { - name: 'ipid', - type: 'keyword', - description: 'IP datagram fragment identifier', - }, - { - name: 'new-fs', - type: 'keyword', - description: 'file system being added to vm', - }, - { - name: 'vm-pid', - type: 'keyword', - description: "vm's process ID", - }, - { - name: 'cap_pi', - type: 'keyword', - description: 'process inherited capability map', - }, - { - name: 'old-auid', - type: 'keyword', - description: 'previous auid value', - }, - { - name: 'oses', - type: 'keyword', - description: "object's session ID", - }, - { - name: 'fd', - type: 'keyword', - description: 'file descriptor number', - }, - { - name: 'igid', - type: 'keyword', - description: "ipc object's group ID", - }, - { - name: 'new-disk', - type: 'keyword', - description: 'disk being added to vm', - }, - { - name: 'parent', - type: 'keyword', - description: 'the inode number of the parent file', - }, - { - name: 'len', - type: 'keyword', - description: 'length', - }, - { - name: 'oflag', - type: 'keyword', - description: 'open syscall flags', - }, - { - name: 'uuid', - type: 'keyword', - description: 'a UUID', - }, - { - name: 'code', - type: 'keyword', - description: 'seccomp action code', - }, - { - name: 'nlnk-grp', - type: 'keyword', - description: 'netlink group number', - }, - { - name: 'cap_fp', - type: 'keyword', - description: 'file permitted capability map', - }, - { - name: 'new-mem', - type: 'keyword', - description: 'new amount of memory in KB', - }, - { - name: 'seperm', - type: 'keyword', - description: 'SELinux permission being decided on', - }, - { - name: 'enforcing', - type: 'keyword', - description: 'new MAC enforcement status', - }, - { - name: 'new-chardev', - type: 'keyword', - description: 'new character device being assigned to vm', - }, - { - name: 'old-rng', - type: 'keyword', - description: 'device name of rng being removed from a vm', - }, - { - name: 'outif', - type: 'keyword', - description: 'out interface number', - }, - { - name: 'cmd', - type: 'keyword', - description: 'command being executed', - }, - { - name: 'hook', - type: 'keyword', - description: 'netfilter hook that packet came from', - }, - { - name: 'new-level', - type: 'keyword', - description: 'new run level', - }, - { - name: 'sauid', - type: 'keyword', - description: 'sent login user ID', - }, - { - name: 'sig', - type: 'keyword', - description: 'signal number', - }, - { - name: 'audit_backlog_wait_time', - type: 'keyword', - description: "audit system's backlog wait time", - }, - { - name: 'printer', - type: 'keyword', - description: 'printer name', - }, - { - name: 'old-mem', - type: 'keyword', - description: 'present amount of memory in KB', - }, - { - name: 'perm', - type: 'keyword', - description: 'the file permission being used', - }, - { - name: 'old_pi', - type: 'keyword', - description: 'old process inherited capability map', - }, - { - name: 'state', - type: 'keyword', - description: 'audit daemon configuration resulting state', - }, - { - name: 'format', - type: 'keyword', - description: "audit log's format", - }, - { - name: 'new_gid', - type: 'keyword', - description: 'new group ID being assigned', - }, - { - name: 'tcontext', - type: 'keyword', - description: "the target's or object's context string", - }, - { - name: 'maj', - type: 'keyword', - description: 'device major number', - }, - { - name: 'watch', - type: 'keyword', - description: 'file name in a watch record', - }, - { - name: 'device', - type: 'keyword', - description: 'device name', - }, - { - name: 'grp', - type: 'keyword', - description: 'group name', - }, - { - name: 'bool', - type: 'keyword', - description: 'name of SELinux boolean', - }, - { - name: 'icmp_type', - type: 'keyword', - description: 'type of icmp message', - }, - { - name: 'new_lock', - type: 'keyword', - description: 'new value of feature lock', - }, - { - name: 'old_prom', - type: 'keyword', - description: 'network promiscuity flag', - }, - { - name: 'acl', - type: 'keyword', - description: 'access mode of resource assigned to vm', - }, - { - name: 'ip', - type: 'keyword', - description: 'network address of a printer', - }, - { - name: 'new_pi', - type: 'keyword', - description: 'new process inherited capability map', - }, - { - name: 'default-context', - type: 'keyword', - description: 'default MAC context', - }, - { - name: 'inode_gid', - type: 'keyword', - description: "group ID of the inode's owner", - }, - { - name: 'new-log_passwd', - type: 'keyword', - description: 'new value for TTY password logging', - }, - { - name: 'new_pe', - type: 'keyword', - description: 'new process effective capability map', - }, - { - name: 'selected-context', - type: 'keyword', - description: 'new MAC context assigned to session', - }, - { - name: 'cap_fver', - type: 'keyword', - description: 'file system capabilities version number', - }, - { - name: 'file', - type: 'keyword', - description: 'file name', - }, - { - name: 'net', - type: 'keyword', - description: 'network MAC address', - }, - { - name: 'virt', - type: 'keyword', - description: 'kind of virtualization being referenced', - }, - { - name: 'cap_pp', - type: 'keyword', - description: 'process permitted capability map', - }, - { - name: 'old-range', - type: 'keyword', - description: 'present SELinux range', - }, - { - name: 'resrc', - type: 'keyword', - description: 'resource being assigned', - }, - { - name: 'new-range', - type: 'keyword', - description: 'new SELinux range', - }, - { - name: 'obj_gid', - type: 'keyword', - description: 'group ID of object', - }, - { - name: 'proto', - type: 'keyword', - description: 'network protocol', - }, - { - name: 'old-disk', - type: 'keyword', - description: 'disk being removed from vm', - }, - { - name: 'audit_failure', - type: 'keyword', - description: "audit system's failure mode", - }, - { - name: 'inif', - type: 'keyword', - description: 'in interface number', - }, - { - name: 'vm', - type: 'keyword', - description: 'virtual machine name', - }, - { - name: 'flags', - type: 'keyword', - description: 'mmap syscall flags', - }, - { - name: 'nlnk-fam', - type: 'keyword', - description: 'netlink protocol number', - }, - { - name: 'old-fs', - type: 'keyword', - description: 'file system being removed from vm', - }, - { - name: 'old-ses', - type: 'keyword', - description: 'previous ses value', - }, - { - name: 'seqno', - type: 'keyword', - description: 'sequence number', - }, - { - name: 'fver', - type: 'keyword', - description: 'file system capabilities version number', - }, - { - name: 'qbytes', - type: 'keyword', - description: 'ipc objects quantity of bytes', - }, - { - name: 'seuser', - type: 'keyword', - description: "user's SE Linux user acct", - }, - { - name: 'cap_fe', - type: 'keyword', - description: 'file assigned effective capability map', - }, - { - name: 'new-vcpu', - type: 'keyword', - description: 'new number of CPU cores', - }, - { - name: 'old-level', - type: 'keyword', - description: 'old run level', - }, - { - name: 'old_pp', - type: 'keyword', - description: 'old process permitted capability map', - }, - { - name: 'daddr', - type: 'keyword', - description: 'remote IP address', - }, - { - name: 'old-role', - type: 'keyword', - description: 'present SELinux role', - }, - { - name: 'ioctlcmd', - type: 'keyword', - description: 'The request argument to the ioctl syscall', - }, - { - name: 'smac', - type: 'keyword', - description: 'local MAC address', - }, - { - name: 'apparmor', - type: 'keyword', - description: 'apparmor event information', - }, - { - name: 'fe', - type: 'keyword', - description: 'file assigned effective capability map', - }, - { - name: 'perm_mask', - type: 'keyword', - description: 'file permission mask that triggered a watch event', - }, - { - name: 'ses', - type: 'keyword', - description: 'login session ID', - }, - { - name: 'cap_fi', - type: 'keyword', - description: 'file inherited capability map', - }, - { - name: 'obj_uid', - type: 'keyword', - description: 'user ID of object', - }, - { - name: 'reason', - type: 'keyword', - description: 'text string denoting a reason for the action', - }, - { - name: 'list', - type: 'keyword', - description: "the audit system's filter list number", - }, - { - name: 'old_lock', - type: 'keyword', - description: 'present value of feature lock', - }, - { - name: 'bus', - type: 'keyword', - description: 'name of subsystem bus a vm resource belongs to', - }, - { - name: 'old_pe', - type: 'keyword', - description: 'old process effective capability map', - }, - { - name: 'new-role', - type: 'keyword', - description: 'new SELinux role', - }, - { - name: 'prom', - type: 'keyword', - description: 'network promiscuity flag', - }, - { - name: 'uri', - type: 'keyword', - description: 'URI pointing to a printer', - }, - { - name: 'audit_enabled', - type: 'keyword', - description: "audit systems's enable/disable status", - }, - { - name: 'old-log_passwd', - type: 'keyword', - description: 'present value for TTY password logging', - }, - { - name: 'old-seuser', - type: 'keyword', - description: 'present SELinux user', - }, - { - name: 'per', - type: 'keyword', - description: 'linux personality', - }, - { - name: 'scontext', - type: 'keyword', - description: "the subject's context string", - }, - { - name: 'tclass', - type: 'keyword', - description: "target's object classification", - }, - { - name: 'ver', - type: 'keyword', - description: "audit daemon's version number", - }, - { - name: 'new', - type: 'keyword', - description: 'value being set in feature', - }, - { - name: 'val', - type: 'keyword', - description: 'generic value associated with the operation', - }, - { - name: 'img-ctx', - type: 'keyword', - description: "the vm's disk image context string", - }, - { - name: 'old-chardev', - type: 'keyword', - description: 'present character device assigned to vm', - }, - { - name: 'old_val', - type: 'keyword', - description: 'current value of SELinux boolean', - }, - { - name: 'success', - type: 'keyword', - description: 'whether the syscall was successful or not', - }, - { - name: 'inode_uid', - type: 'keyword', - description: "user ID of the inode's owner", - }, - { - name: 'removed', - type: 'keyword', - description: 'number of deleted files', - }, - { - name: 'socket', - type: 'group', - fields: [ - { - name: 'port', - type: 'keyword', - description: 'The port number.', - }, - { - name: 'saddr', - type: 'keyword', - description: 'The raw socket address structure.', - }, - { - name: 'addr', - type: 'keyword', - description: 'The remote address.', - }, - { - name: 'family', - type: 'keyword', - example: 'unix', - description: 'The socket family (unix, ipv4, ipv6, netlink).', - }, - { - name: 'path', - type: 'keyword', - description: 'This is the path associated with a unix socket.', - }, - ], - }, - ], - }, - { - name: 'messages', - type: 'alias', - migration: true, - path: 'event.original', - description: - 'An ordered list of the raw messages received from the kernel that were used to construct this document. This field is present if an error occurred processing the data or if `include_raw_message` is set in the config.\n', - }, - { - name: 'warnings', - type: 'alias', - migration: true, - path: 'error.message', - description: - 'The warnings generated by the Beat during the construction of the event. These are disabled by default and are used for development and debug purposes only.\n', - }, - ], - }, - { - name: 'geoip', - type: 'group', - description: - 'The geoip fields are defined as a convenience in case you decide to enrich the data using a geoip filter in Logstash or Ingest Node.\n', - fields: [ - { - name: 'continent_name', - type: 'keyword', - description: 'The name of the continent.\n', - }, - { - name: 'city_name', - type: 'keyword', - description: 'The name of the city.\n', - }, - { - name: 'region_name', - type: 'keyword', - description: 'The name of the region.\n', - }, - { - name: 'country_iso_code', - type: 'keyword', - description: 'Country ISO code.\n', - }, - { - name: 'location', - type: 'geo_point', - description: 'The longitude and latitude.\n', - }, - ], - }, - ], - }, - { - key: 'file_integrity', - title: 'File Integrity', - description: 'These are the fields generated by the file_integrity module.', - fields: [ - { - name: 'hash', - type: 'group', - description: - 'Hashes of the file. The keys are algorithm names and the values are the hex encoded digest values.\n', - fields: [ - { - name: 'blake2b_256', - type: 'keyword', - description: 'BLAKE2b-256 hash of the file.', - }, - { - name: 'blake2b_384', - type: 'keyword', - description: 'BLAKE2b-384 hash of the file.', - }, - { - name: 'blake2b_512', - type: 'keyword', - description: 'BLAKE2b-512 hash of the file.', - }, - { - name: 'md5', - overwrite: true, - type: 'keyword', - description: 'MD5 hash of the file.', - }, - { - name: 'sha1', - overwrite: true, - type: 'keyword', - description: 'SHA1 hash of the file.', - }, - { - name: 'sha224', - type: 'keyword', - description: 'SHA224 hash of the file.', - }, - { - name: 'sha256', - overwrite: true, - type: 'keyword', - description: 'SHA256 hash of the file.', - }, - { - name: 'sha384', - type: 'keyword', - description: 'SHA384 hash of the file.', - }, - { - name: 'sha3_224', - type: 'keyword', - description: 'SHA3_224 hash of the file.', - }, - { - name: 'sha3_256', - type: 'keyword', - description: 'SHA3_256 hash of the file.', - }, - { - name: 'sha3_384', - type: 'keyword', - description: 'SHA3_384 hash of the file.', - }, - { - name: 'sha3_512', - type: 'keyword', - description: 'SHA3_512 hash of the file.', - }, - { - name: 'sha512', - overwrite: true, - type: 'keyword', - description: 'SHA512 hash of the file.', - }, - { - name: 'sha512_224', - type: 'keyword', - description: 'SHA512/224 hash of the file.', - }, - { - name: 'sha512_256', - type: 'keyword', - description: 'SHA512/256 hash of the file.', - }, - { - name: 'xxh64', - type: 'keyword', - description: 'XX64 hash of the file.', - }, - ], - }, - ], - }, - { - key: 'system', - title: 'System', - description: 'These are the fields generated by the system module.\n', - release: 'beta', - fields: [ - { - name: 'event', - type: 'group', - fields: [ - { - name: 'origin', - type: 'keyword', - description: - 'Origin of the event. This can be a file path (e.g. `/var/log/log.1`), or the name of the system component that supplied the data (e.g. `netlink`).\n', - }, - ], - }, - { - name: 'user', - type: 'group', - fields: [ - { - name: 'entity_id', - type: 'keyword', - description: - 'ID uniquely identifying the user on a host. It is computed as a SHA-256 hash of the host ID, user ID, and user name.\n', - }, - { - name: 'terminal', - type: 'keyword', - description: 'Terminal of the user.\n', - }, - ], - }, - { - name: 'process', - type: 'group', - fields: [ - { - name: 'hash', - type: 'group', - description: - 'Hashes of the executable. The keys are algorithm names and the values are the hex encoded digest values.\n', - fields: [ - { - name: 'blake2b_256', - type: 'keyword', - description: 'BLAKE2b-256 hash of the executable.', - }, - { - name: 'blake2b_384', - type: 'keyword', - description: 'BLAKE2b-384 hash of the executable.', - }, - { - name: 'blake2b_512', - type: 'keyword', - description: 'BLAKE2b-512 hash of the executable.', - }, - { - name: 'sha224', - type: 'keyword', - description: 'SHA224 hash of the executable.', - }, - { - name: 'sha384', - type: 'keyword', - description: 'SHA384 hash of the executable.', - }, - { - name: 'sha3_224', - type: 'keyword', - description: 'SHA3_224 hash of the executable.', - }, - { - name: 'sha3_256', - type: 'keyword', - description: 'SHA3_256 hash of the executable.', - }, - { - name: 'sha3_384', - type: 'keyword', - description: 'SHA3_384 hash of the executable.', - }, - { - name: 'sha3_512', - type: 'keyword', - description: 'SHA3_512 hash of the executable.', - }, - { - name: 'sha512_224', - type: 'keyword', - description: 'SHA512/224 hash of the executable.', - }, - { - name: 'sha512_256', - type: 'keyword', - description: 'SHA512/256 hash of the executable.', - }, - { - name: 'xxh64', - type: 'keyword', - description: 'XX64 hash of the executable.', - }, - ], - }, - ], - }, - { - name: 'socket', - type: 'group', - fields: [ - { - name: 'entity_id', - type: 'keyword', - description: - 'ID uniquely identifying the socket. It is computed as a SHA-256 hash of the host ID, socket inode, local IP, local port, remote IP, and remote port.\n', - }, - ], - }, - { - name: 'system.audit', - type: 'group', - description: '\n', - fields: [ - { - name: 'host', - type: 'group', - description: '`host` contains general host information.\n', - release: 'beta', - fields: [ - { - name: 'uptime', - type: 'long', - format: 'duration', - input_format: 'nanoseconds', - output_format: 'asDays', - output_precision: 1, - description: 'Uptime in nanoseconds.\n', - }, - { - name: 'boottime', - type: 'date', - description: 'Boot time.\n', - }, - { - name: 'containerized', - type: 'boolean', - description: 'Set if host is a container.\n', - }, - { - name: 'timezone.name', - type: 'keyword', - description: 'Name of the timezone of the host, e.g. BST.\n', - }, - { - name: 'timezone.offset.sec', - type: 'long', - description: 'Timezone offset in seconds.\n', - }, - { - name: 'hostname', - type: 'keyword', - description: 'Hostname.\n', - }, - { - name: 'id', - type: 'keyword', - description: 'Host ID.\n', - }, - { - name: 'architecture', - type: 'keyword', - description: 'Host architecture (e.g. x86_64).\n', - }, - { - name: 'mac', - type: 'keyword', - description: 'MAC addresses.\n', - }, - { - name: 'ip', - type: 'ip', - description: 'IP addresses.\n', - }, - { - name: 'os', - type: 'group', - description: '`os` contains information about the operating system.\n', - fields: [ - { - name: 'codename', - type: 'keyword', - description: 'OS codename, if any (e.g. stretch).\n', - }, - { - name: 'platform', - type: 'keyword', - description: 'OS platform (e.g. centos, ubuntu, windows).\n', - }, - { - name: 'name', - type: 'keyword', - description: 'OS name (e.g. Mac OS X).\n', - }, - { - name: 'family', - type: 'keyword', - description: 'OS family (e.g. redhat, debian, freebsd, windows).\n', - }, - { - name: 'version', - type: 'keyword', - description: 'OS version.\n', - }, - { - name: 'kernel', - type: 'keyword', - description: "The operating system's kernel version.\n", - }, - ], - }, - ], - }, - { - name: 'package', - type: 'group', - description: '`package` contains information about an installed or removed package.\n', - release: 'beta', - fields: [ - { - name: 'entity_id', - type: 'keyword', - description: - 'ID uniquely identifying the package. It is computed as a SHA-256 hash of the host ID, package name, and package version.\n', - }, - { - name: 'name', - type: 'keyword', - description: 'Package name.\n', - }, - { - name: 'version', - type: 'keyword', - description: 'Package version.\n', - }, - { - name: 'release', - type: 'keyword', - description: 'Package release.\n', - }, - { - name: 'arch', - type: 'keyword', - description: 'Package architecture.\n', - }, - { - name: 'license', - type: 'keyword', - description: 'Package license.\n', - }, - { - name: 'installtime', - type: 'date', - description: 'Package install time.\n', - }, - { - name: 'size', - type: 'long', - description: 'Package size.\n', - }, - { - name: 'summary', - description: 'Package summary.\n', - }, - { - name: 'url', - type: 'keyword', - description: 'Package URL.\n', - }, - ], - }, - { - name: 'user', - type: 'group', - description: '`user` contains information about the users on a system.\n', - release: 'beta', - fields: [ - { - name: 'name', - type: 'keyword', - description: 'User name.\n', - }, - { - name: 'uid', - type: 'keyword', - description: 'User ID.\n', - }, - { - name: 'gid', - type: 'keyword', - description: 'Group ID.\n', - }, - { - name: 'dir', - type: 'keyword', - description: "User's home directory.\n", - }, - { - name: 'shell', - type: 'keyword', - description: 'Program to run at login.\n', - }, - { - name: 'user_information', - type: 'keyword', - description: 'General user information. On Linux, this is the gecos field.\n', - }, - { - name: 'group', - type: 'object', - description: - "`group` contains information about any groups the user is part of (beyond the user's primary group).\n", - fields: [ - { - name: 'name', - type: 'keyword', - description: 'Group name.\n', - }, - { - name: 'gid', - type: 'integer', - description: 'Group ID.\n', - }, - ], - }, - { - name: 'password', - type: 'group', - description: - "`password` contains information about a user's password (not the password itself).\n", - fields: [ - { - name: 'type', - type: 'keyword', - description: - "A user's password type. Possible values are `shadow_password` (the password hash is in the shadow file), `password_disabled`, `no_password` (this is dangerous as anyone can log in), and `crypt_password` (when the password field in /etc/passwd seems to contain an encrypted password).\n", - }, - { - name: 'last_changed', - type: 'date', - description: "The day the user's password was last changed.\n", - }, - ], - }, - ], - }, - ], - }, - ], - }, -]; diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/ecs.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/ecs.ts deleted file mode 100644 index a439d105d63df..0000000000000 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/ecs.ts +++ /dev/null @@ -1,5675 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -/** - * An instance of the unmodified schema exported from https://github.com/elastic/ecs - * A map of `EcsNamespace.name` `->` `EcsNamespace` - * - * - NOTE: This instance does NOT include "virtual (non-spec)" ECS fields e.g `_id`. - * - NOTE: This instance does NOT include "mappings" from ECS fields, to `ECS` - * instances e.g. `@timestamp` to `timestamp` - */ - -import { Schema } from '../type'; - -export const ecsSchema: Schema = [ - { - key: 'ecs', - title: 'ECS', - description: 'ECS Fields.', - fields: [ - { - name: '@timestamp', - level: 'core', - required: true, - type: 'date', - description: - 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', - example: '2016-05-23T08:05:34.853Z', - }, - { - name: 'labels', - level: 'core', - type: 'object', - object_type: 'keyword', - description: - 'Custom key/value pairs.\n\nCan be used to add meta information to events. Should not contain nested objects.\nAll values are stored as keyword.\n\nExample: `docker` and `k8s` labels.', - example: '{"application": "foo-bar", "env": "production"}', - }, - { - name: 'message', - level: 'core', - type: 'text', - description: - 'For log events the message field contains the log message, optimized\nfor viewing in a log viewer.\n\nFor structured logs without an original message field, other fields can be concatenated\nto form a human-readable summary of the event.\n\nIf multiple messages exist, they can be combined into one message.', - example: 'Hello World', - }, - { - name: 'tags', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'List of keywords used to tag each event.', - example: '["production", "env2"]', - }, - { - name: 'agent', - title: 'Agent', - group: 2, - description: - 'The agent fields contain the data about the software entity, if\nany, that collects, detects, or observes events on a host, or takes measurements\non a host.\n\nExamples include Beats. Agents may also run on observers. ECS agent.* fields\nshall be populated with details of the agent running on the host or observer\nwhere the event happened or the measurement was taken.', - footnote: - 'Examples: In the case of Beats for logs, the agent.name is filebeat.\nFor APM, it is the agent running in the app/service. The agent information does\nnot change if data is sent through queuing systems like Kafka, Redis, or processing\nsystems such as Logstash or APM Server.', - type: 'group', - fields: [ - { - name: 'build.original', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Extended build information for the agent.\n\nThis field is intended to contain any build information that a data source\nmay provide, no specific formatting is required.', - example: - 'metricbeat version 7.6.0 (amd64), libbeat 7.6.0 [6a23e8f8f30f5001ba344e4e54d8d9cb82cb107c\nbuilt 2020-02-05 23:10:10 +0000 UTC]', - default_field: false, - }, - { - name: 'ephemeral_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Ephemeral identifier of this agent (if one exists).\n\nThis id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier of this agent (if one exists).\n\nExample: For Beats this would be beat.id.', - example: '8a4f500d', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', - example: 'foo', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', - example: 'filebeat', - }, - { - name: 'version', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Version of the agent.', - example: '6.0.0-rc2', - }, - ], - }, - { - name: 'as', - title: 'Autonomous System', - group: 2, - description: - 'An autonomous system (AS) is a collection of connected Internet Protocol\n(IP) routing prefixes under the control of one or more network operators on\nbehalf of a single administrative entity or domain that presents a common, clearly\ndefined routing policy to the internet.', - type: 'group', - fields: [ - { - name: 'number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - ], - }, - { - name: 'client', - title: 'Client', - group: 2, - description: - 'A client is defined as the initiator of a network connection for\nevents regarding sessions, connections, or bidirectional flow records.\n\nFor TCP events, the client is the initiator of the TCP connection that sends\nthe SYN packet(s). For other protocols, the client is generally the initiator\nor requestor in the network transaction. Some systems use the term "originator"\nto refer the client in TCP connections. The client fields describe details about\nthe system acting as the client in the network event. Client fields are usually\npopulated in conjunction with server fields. Client fields are generally not\npopulated for packet-level events.\n\nClient / server representations can add semantic context to an exchange, which\nis helpful to visualize the data in certain situations. If your context falls\nin that category, you should still ensure that source and destination are filled\nappropriately.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Some event client addresses are defined ambiguously. The event\nwill sometimes list an IP, a domain or a unix socket. You should always store\nthe raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'as.number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'as.organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the client to the server.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Client domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: 'IP address of the client (IPv4 or IPv6).', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC address of the client.', - }, - { - name: 'nat.ip', - level: 'extended', - type: 'ip', - description: - 'Translated IP of source based NAT sessions (e.g. internal client\nto internet).\n\nTypically connections traversing load balancers, firewalls, or routers.', - }, - { - name: 'nat.port', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Translated port of source based NAT sessions (e.g. internal client\nto internet).\n\nTypically connections traversing load balancers, firewalls, or routers.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the client to the server.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the client.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered client domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'cloud', - title: 'Cloud', - group: 2, - description: 'Fields related to the cloud or infrastructure the events are coming\nfrom.', - footnote: - 'Examples: If Metricbeat is running on an EC2 host and fetches data\nfrom its host, the cloud info contains the data about this machine. If Metricbeat\nruns on a remote machine outside the cloud and fetches data from a service running\nin the cloud, the field contains cloud data from the machine the service is\nrunning on.', - type: 'group', - fields: [ - { - name: 'account.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The cloud account or organization id used to identify different\nentities in a multi-tenant environment.\n\nExamples: AWS account id, Google Cloud ORG Id, or other unique identifier.', - example: 666777888999, - }, - { - name: 'availability_zone', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Availability zone in which this host is running.', - example: 'us-east-1c', - }, - { - name: 'instance.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Instance ID of the host machine.', - example: 'i-1234567890abcdef0', - }, - { - name: 'instance.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Instance name of the host machine.', - }, - { - name: 'machine.type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Machine type of the host machine.', - example: 't2.medium', - }, - { - name: 'provider', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the cloud provider. Example values are aws, azure, gcp,\nor digitalocean.', - example: 'aws', - }, - { - name: 'region', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Region in which this host is running.', - example: 'us-east-1', - }, - ], - }, - { - name: 'code_signature', - title: 'Code Signature', - group: 2, - description: 'These fields contain information about binary code signatures.', - type: 'group', - fields: [ - { - name: 'exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - ], - }, - { - name: 'container', - title: 'Container', - group: 2, - description: - 'Container fields are used for meta information about the specific\ncontainer that is the source of information.\n\nThese fields help correlate data based containers from any runtime.', - type: 'group', - fields: [ - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique container id.', - }, - { - name: 'image.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the image the container was built on.', - }, - { - name: 'image.tag', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Container image tags.', - }, - { - name: 'labels', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: 'Image labels.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Container name.', - }, - { - name: 'runtime', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Runtime managing this container.', - example: 'docker', - }, - ], - }, - { - name: 'destination', - title: 'Destination', - group: 2, - description: - 'Destination fields describe details about the destination of a packet/event.\n\nDestination fields are usually populated in conjunction with source fields.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Some event destination addresses are defined ambiguously. The\nevent will sometimes list an IP, a domain or a unix socket. You should always\nstore the raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'as.number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'as.organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the destination to the source.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Destination domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: 'IP address of the destination (IPv4 or IPv6).', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC address of the destination.', - }, - { - name: 'nat.ip', - level: 'extended', - type: 'ip', - description: - 'Translated ip of destination based NAT sessions (e.g. internet\nto private DMZ)\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'nat.port', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Port the source session is translated to by NAT Device.\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the destination to the source.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the destination.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered destination domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'dll', - title: 'DLL', - group: 2, - description: - 'These fields contain information about code libraries dynamically\nloaded into processes.\n\n\nMany operating systems refer to "shared code libraries" with different names,\nbut this field set refers to all of the following:\n\n* Dynamic-link library (`.dll`) commonly used on Windows\n\n* Shared Object (`.so`) commonly used on Unix-like operating systems\n\n* Dynamic library (`.dylib`) commonly used on macOS', - type: 'group', - fields: [ - { - name: 'code_signature.exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'code_signature.subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'code_signature.trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - { - name: 'hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - default_field: false, - }, - { - name: 'hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - default_field: false, - }, - { - name: 'hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - default_field: false, - }, - { - name: 'hash.sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - default_field: false, - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the library.\n\nThis generally maps to the name of the file on disk.', - example: 'kernel32.dll', - default_field: false, - }, - { - name: 'path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Full file path of the library.', - example: 'C:\\Windows\\System32\\kernel32.dll', - default_field: false, - }, - { - name: 'pe.architecture', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'CPU architecture target for the file.', - example: 'x64', - default_field: false, - }, - { - name: 'pe.company', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal company name of the file, provided at compile-time.', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'pe.description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal description of the file, provided at compile-time.', - example: 'Paint', - default_field: false, - }, - { - name: 'pe.file_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal version of the file, provided at compile-time.', - example: '6.3.9600.17415', - default_field: false, - }, - { - name: 'pe.imphash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A hash of the imports in a PE file. An imphash -- or import hash\n-- can be used to fingerprint binaries even after recompilation or other code-level\ntransformations have occurred, which would change more traditional hash values.\n\nLearn more at https://www.fireeye.com/blog/threat-research/2014/01/tracking-malware-import-hashing.html.', - example: '0c6803c4e922103c4dca5963aad36ddf', - default_field: false, - }, - { - name: 'pe.original_file_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal name of the file, provided at compile-time.', - example: 'MSPAINT.EXE', - default_field: false, - }, - { - name: 'pe.product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal product name of the file, provided at compile-time.', - example: 'Microsoft® Windows® Operating System', - default_field: false, - }, - ], - }, - { - name: 'dns', - title: 'DNS', - group: 2, - description: - 'Fields describing DNS queries and answers.\n\nDNS events should either represent a single DNS query prior to getting answers\n(`dns.type:query`) or they should represent a full exchange and contain the\nquery details as well as all of the answers that were provided for this query\n(`dns.type:answer`).', - type: 'group', - fields: [ - { - name: 'answers', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'An array containing an object for each answer section returned\nby the server.\n\nThe main keys that should be present in these objects are defined by ECS.\nRecords that have more information may contain more keys than what ECS defines.\n\nNot all DNS data sources give all details about DNS answers. At minimum, answer\nobjects must contain the `data` key. If more information is available, map\nas much of it to ECS as possible, and add any additional fields to the answer\nobjects as custom fields.', - }, - { - name: 'answers.class', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The class of DNS data contained in this resource record.', - example: 'IN', - }, - { - name: 'answers.data', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The data describing the resource.\n\nThe meaning of this data depends on the type and class of the resource record.', - example: '10.10.10.10', - }, - { - name: 'answers.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The domain name to which this resource record pertains.\n\nIf a chain of CNAME is being resolved, each answer `name` should be the\none that corresponds with the answer `data`. It should not simply be the\noriginal `question.name` repeated.', - example: 'www.google.com', - }, - { - name: 'answers.ttl', - level: 'extended', - type: 'long', - description: - 'The time interval in seconds that this resource record may be cached\nbefore it should be discarded. Zero values mean that the data should not be\ncached.', - example: 180, - }, - { - name: 'answers.type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The type of data contained in this resource record.', - example: 'CNAME', - }, - { - name: 'header_flags', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of 2 letter DNS header flags.\n\nExpected values are: AA, TC, RD, RA, AD, CD, DO.', - example: ['RD', 'RA'], - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The DNS packet identifier assigned by the program that generated\nthe query. The identifier is copied to the response.', - example: 62111, - }, - { - name: 'op_code', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The DNS operation code that specifies the kind of query in the\nmessage. This value is set by the originator of a query and copied into the\nresponse.', - example: 'QUERY', - }, - { - name: 'question.class', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The class of records being queried.', - example: 'IN', - }, - { - name: 'question.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The name being queried.\n\nIf the name field contains non-printable characters (below 32 or above 126),\nthose characters should be represented as escaped base 10 integers (\\DDD).\nBack slashes and quotes should be escaped. Tabs, carriage returns, and line\nfeeds should be converted to \\t, \\r, and \\n respectively.', - example: 'www.google.com', - }, - { - name: 'question.registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'question.subdomain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The subdomain is all of the labels under the registered_domain.\n\nIf the domain has multiple levels of subdomain, such as "sub2.sub1.example.com",\nthe subdomain field should contain "sub2.sub1", with no trailing period.', - example: 'www', - }, - { - name: 'question.top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'question.type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The type of record being queried.', - example: 'AAAA', - }, - { - name: 'resolved_ip', - level: 'extended', - type: 'ip', - description: - 'Array containing all IPs seen in `answers.data`.\n\nThe `answers` array can be difficult to use, because of the variety of data\nformats it can contain. Extracting all IP addresses seen in there to `dns.resolved_ip`\nmakes it possible to index them as IP addresses, and makes them easier to\nvisualize and query for.', - example: ['10.10.10.10', '10.10.10.11'], - }, - { - name: 'response_code', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The DNS response code.', - example: 'NOERROR', - }, - { - name: 'type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of DNS event captured, query or answer.\n\nIf your source of DNS events only gives you DNS queries, you should only create\ndns events of type `dns.type:query`.\n\nIf your source of DNS events gives you answers as well, you should create\none event per query (optionally as soon as the query is seen). And a second\nevent containing all query details as well as an array of answers.', - example: 'answer', - }, - ], - }, - { - name: 'ecs', - title: 'ECS', - group: 2, - description: 'Meta-information specific to ECS.', - type: 'group', - fields: [ - { - name: 'version', - level: 'core', - required: true, - type: 'keyword', - ignore_above: 1024, - description: - 'ECS version this event conforms to. `ecs.version` is a required\nfield and must exist in all events.\n\nWhen querying across multiple indices -- which may conform to slightly different\nECS versions -- this field lets integrations adjust to the schema version\nof the events.', - example: '1.0.0', - }, - ], - }, - { - name: 'error', - title: 'Error', - group: 2, - description: - 'These fields can represent errors of any kind.\n\nUse them for errors that happen while fetching events or in cases where the\nevent itself contains an error.', - type: 'group', - fields: [ - { - name: 'code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Error code describing the error.', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the error.', - }, - { - name: 'message', - level: 'core', - type: 'text', - description: 'Error message.', - }, - { - name: 'stack_trace', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'The stack trace of this error in plain text.', - }, - { - name: 'type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The type of the error, for example the class name of the exception.', - example: 'java.lang.NullPointerException', - }, - ], - }, - { - name: 'event', - title: 'Event', - group: 2, - description: - 'The event fields are used for context information about the log\nor metric event itself.\n\nA log is defined as an event containing details of something that happened.\nLog events must include the time at which the thing happened. Examples of log\nevents include a process starting on a host, a network packet being sent from\na source to a destination, or a network connection between a client and a server\nbeing initiated or closed. A metric is defined as an event containing one or\nmore numerical measurements and the time at which the measurement was taken.\nExamples of metric events include memory pressure measured on a host and device\ntemperature. See the `event.kind` definition in this section for additional\ndetails about metric and state events.', - type: 'group', - fields: [ - { - name: 'action', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.', - example: 'user-password-change', - }, - { - name: 'category', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is one of four ECS Categorization Fields, and indicates the\nsecond level in the ECS category hierarchy.\n\n`event.category` represents the "big buckets" of ECS categories. For example,\nfiltering on `event.category:process` yields all events relating to process\nactivity. This field is closely related to `event.type`, which is used as\na subcategory.\n\nThis field is an array. This will allow proper categorization of some events\nthat fall in multiple categories.', - example: 'authentication', - }, - { - name: 'code', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Identification code for this event, if one exists.\n\nSome event sources use event codes to identify messages unambiguously, regardless\nof message language or wording adjustments over time. An example of this is\nthe Windows Event ID.', - example: 4648, - }, - { - name: 'created', - level: 'core', - type: 'date', - description: - 'event.created contains the date/time when the event was first\nread by an agent, or by your pipeline.\n\nThis field is distinct from @timestamp in that @timestamp typically contain\nthe time extracted from the original event.\n\nIn most situations, these two timestamps will be slightly different. The difference\ncan be used to calculate the delay between your source generating an event,\nand the time when your agent first processed it. This can be used to monitor\nyour agent or pipeline ability to keep up with your event source.\n\nIn case the two timestamps are identical, @timestamp should be used.', - example: '2016-05-23T08:05:34.857Z', - }, - { - name: 'dataset', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the dataset.\n\nIf an event source publishes more than one type of log or events (e.g. access\nlog, error log), the dataset is used to specify which one the event comes\nfrom.\n\nIt is recommended but not required to start the dataset name with the module\nname, followed by a dot, then the dataset name.', - example: 'apache.access', - }, - { - name: 'duration', - level: 'core', - type: 'long', - format: 'duration', - input_format: 'nanoseconds', - output_format: 'asMilliseconds', - output_precision: 1, - description: - 'Duration of the event in nanoseconds.\n\nIf event.start and event.end are known this value should be the difference\nbetween the end and start time.', - }, - { - name: 'end', - level: 'extended', - type: 'date', - description: - 'event.end contains the date when the event ended or when the activity\nwas last observed.', - }, - { - name: 'hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Hash (perhaps logstash fingerprint) of raw field to be able to\ndemonstrate log integrity.', - example: '123456789012345678901234567890ABCD', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique ID to describe the event.', - example: '8a4f500d', - }, - { - name: 'ingested', - level: 'core', - type: 'date', - description: - 'Timestamp when an event arrived in the central data store.\n\nThis is different from `@timestamp`, which is when the event originally occurred. It is\nalso different from `event.created`, which is meant to capture the first time\nan agent saw the event.\n\nIn normal conditions, assuming no tampering, the timestamps should chronologically\nlook like this: `@timestamp` < `event.created` < `event.ingested`.', - example: '2016-05-23T08:05:35.101Z', - default_field: false, - }, - { - name: 'kind', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is one of four ECS Categorization Fields, and indicates the\nhighest level in the ECS category hierarchy.\n\n`event.kind` gives high-level information about what type of information the\nevent contains, without being specific to the contents of the event. For example,\nvalues of this field distinguish alert events from metric events.\n\nThe value of this field can be used to inform how these kinds of events should\nbe handled. They may warrant different retention, different access control,\nit may also help understand whether the data coming in at a regular interval\nor not.', - example: 'alert', - }, - { - name: 'module', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the module this data is coming from.\n\nIf your monitoring agent supports the concept of modules or plugins to process\nevents of a given source (e.g. Apache logs), `event.module` should contain\nthe name of this module.', - example: 'apache', - }, - { - name: 'original', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Raw text message of entire event. Used to demonstrate log integrity.\n\nThis field is not indexed and doc_values are disabled. It cannot be searched,\nbut it can be retrieved from `_source`.', - example: - 'Sep 19 08:26:10 host CEF:0|Security| threatmanager|1.0|100|\nworm successfully stopped|10|src=10.0.0.1 dst=2.1.2.2spt=1232', - }, - { - name: 'outcome', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is one of four ECS Categorization Fields, and indicates the\nlowest level in the ECS category hierarchy.\n\n`event.outcome` simply denotes whether the event represents a success or a\nfailure from the perspective of the entity that produced the event.\n\nNote that when a single transaction is described in multiple events, each\nevent may populate different values of `event.outcome`, according to their\nperspective.\n\nAlso note that in the case of a compound event (a single event that contains\nmultiple logical events), this field should be populated with the value that\nbest captures the overall success or failure from the perspective of the event\nproducer.\n\nFurther note that not all events will have an associated outcome. For example,\nthis field is generally not populated for metric events, events with `event.type:info`,\nor any events for which an outcome does not make logical sense.', - example: 'success', - }, - { - name: 'provider', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Source of the event.\n\nEvent transports such as Syslog or the Windows Event Log typically mention\nthe source of an event. It can be the name of the software that generated\nthe event (e.g. Sysmon, httpd), or of a subsystem of the operating system\n(kernel, Microsoft-Windows-Security-Auditing).', - example: 'kernel', - }, - { - name: 'reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Reference URL linking to additional information about this event.\n\nThis URL links to a static definition of the this event. Alert events, indicated\nby `event.kind:alert`, are a common use case for this field.', - example: 'https://system.vendor.com/event/#0001234', - default_field: false, - }, - { - name: 'risk_score', - level: 'core', - type: 'float', - description: - "Risk score or priority of the event (e.g. security solutions).\nUse your system's original value here.", - }, - { - name: 'risk_score_norm', - level: 'extended', - type: 'float', - description: - 'Normalized risk score or priority of the event, on a scale of\n0 to 100.\n\nThis is mainly useful if you use more than one system that assigns risk scores,\nand you want to see a normalized value across all systems.', - }, - { - name: 'sequence', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Sequence number of the event.\n\nThe sequence number is a value published by some event sources, to make the\nexact ordering of events unambiguous, regardless of the timestamp precision.', - }, - { - name: 'severity', - level: 'core', - type: 'long', - format: 'string', - description: - 'The numeric severity of the event according to your event source.\n\nWhat the different severity values mean can be different between sources and\nuse cases. It is up to the implementer to make sure severities are consistent\nacross events from the same source.\n\nThe Syslog severity belongs in `log.syslog.severity.code`. `event.severity`\nis meant to represent the severity according to the event source (e.g. firewall,\nIDS). If the event source does not publish its own severity, you may optionally\ncopy the `log.syslog.severity.code` to `event.severity`.', - example: 7, - }, - { - name: 'start', - level: 'extended', - type: 'date', - description: - 'event.start contains the date when the event started or when the\nactivity was first observed.', - }, - { - name: 'timezone', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'This field should be populated when the event timestamp does\nnot include timezone information already (e.g. default Syslog timestamps).\nIt is optional otherwise.\n\nAcceptable timezone formats are: a canonical ID (e.g. "Europe/Amsterdam"),\nabbreviated (e.g. "EST") or an HH:mm differential (e.g. "-05:00").', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is one of four ECS Categorization Fields, and indicates the\nthird level in the ECS category hierarchy.\n\n`event.type` represents a categorization "sub-bucket" that, when used along\nwith the `event.category` field values, enables filtering events down to a\nlevel appropriate for single visualization.\n\nThis field is an array. This will allow proper categorization of some events\nthat fall in multiple event types.', - }, - { - name: 'url', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'URL linking to an external system to continue investigation of\nthis event.\n\nThis URL links to another system where in-depth investigation of the specific\noccurence of this event can take place. Alert events, indicated by `event.kind:alert`,\nare a common use case for this field.', - example: 'https://mysystem.mydomain.com/alert/5271dedb-f5b0-4218-87f0-4ac4870a38fe', - default_field: false, - }, - ], - }, - { - name: 'file', - title: 'File', - group: 2, - description: - 'A file is defined as a set of information that has been created\non, or has existed on a filesystem.\n\nFile objects can be associated with host events, network events, and/or file\nevents (e.g., those produced by File Integrity Monitoring [FIM] products or\nservices). File fields provide details about the affected file associated with\nthe event or metric.', - type: 'group', - fields: [ - { - name: 'accessed', - level: 'extended', - type: 'date', - description: - 'Last time the file was accessed.\n\nNote that not all filesystems keep track of access time.', - }, - { - name: 'attributes', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of file attributes.\n\nAttributes names will vary by platform. Here is a non-exhaustive list of values\nthat are expected in this field: archive, compressed, directory, encrypted,\nexecute, hidden, read, readonly, system, write.', - example: '["readonly", "system"]', - default_field: false, - }, - { - name: 'code_signature.exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'code_signature.subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'code_signature.trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - { - name: 'created', - level: 'extended', - type: 'date', - description: - 'File creation time.\n\nNote that not all filesystems store the creation time.', - }, - { - name: 'ctime', - level: 'extended', - type: 'date', - description: - 'Last time the file attributes or metadata changed.\n\nNote that changes to the file content will update `mtime`. This implies `ctime`\nwill be adjusted at the same time, since `mtime` is an attribute of the file.', - }, - { - name: 'device', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Device that is the source of the file.', - example: 'sda', - }, - { - name: 'directory', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Directory where the file is located. It should include the drive\nletter, when appropriate.', - example: '/home/alice', - }, - { - name: 'drive_letter', - level: 'extended', - type: 'keyword', - ignore_above: 1, - description: - 'Drive letter where the file is located. This field is only relevant\non Windows.\n\nThe value should be uppercase, and not include the colon.', - example: 'C', - default_field: false, - }, - { - name: 'extension', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'File extension.', - example: 'png', - }, - { - name: 'gid', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Primary group ID (GID) of the file.', - example: '1001', - }, - { - name: 'group', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Primary group name of the file.', - example: 'alice', - }, - { - name: 'hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - }, - { - name: 'hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - }, - { - name: 'hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - }, - { - name: 'hash.sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - }, - { - name: 'inode', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Inode representing the file in the filesystem.', - example: '256383', - }, - { - name: 'mime_type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'MIME type should identify the format of the file or stream of bytes\nusing https://www.iana.org/assignments/media-types/media-types.xhtml[IANA\nofficial types], where possible. When more than one type is applicable, the\nmost specific type should be used.', - default_field: false, - }, - { - name: 'mode', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Mode of the file in octal representation.', - example: '0640', - }, - { - name: 'mtime', - level: 'extended', - type: 'date', - description: 'Last time the file content was modified.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the file including the extension, without the directory.', - example: 'example.png', - }, - { - name: 'owner', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: "File owner's username.", - example: 'alice', - }, - { - name: 'path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'Full path to the file, including the file name. It should include\nthe drive letter, when appropriate.', - example: '/home/alice/example.png', - }, - { - name: 'pe.architecture', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'CPU architecture target for the file.', - example: 'x64', - default_field: false, - }, - { - name: 'pe.company', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal company name of the file, provided at compile-time.', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'pe.description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal description of the file, provided at compile-time.', - example: 'Paint', - default_field: false, - }, - { - name: 'pe.file_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal version of the file, provided at compile-time.', - example: '6.3.9600.17415', - default_field: false, - }, - { - name: 'pe.imphash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A hash of the imports in a PE file. An imphash -- or import hash\n-- can be used to fingerprint binaries even after recompilation or other code-level\ntransformations have occurred, which would change more traditional hash values.\n\nLearn more at https://www.fireeye.com/blog/threat-research/2014/01/tracking-malware-import-hashing.html.', - example: '0c6803c4e922103c4dca5963aad36ddf', - default_field: false, - }, - { - name: 'pe.original_file_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal name of the file, provided at compile-time.', - example: 'MSPAINT.EXE', - default_field: false, - }, - { - name: 'pe.product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal product name of the file, provided at compile-time.', - example: 'Microsoft® Windows® Operating System', - default_field: false, - }, - { - name: 'size', - level: 'extended', - type: 'long', - description: 'File size in bytes.\n\nOnly relevant when `file.type` is "file".', - example: 16384, - }, - { - name: 'target_path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Target path for symlinks.', - }, - { - name: 'type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'File type (file, dir, or symlink).', - example: 'file', - }, - { - name: 'uid', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The user ID (UID) or security identifier (SID) of the file owner.', - example: '1001', - }, - ], - }, - { - name: 'geo', - title: 'Geo', - group: 2, - description: - 'Geo fields can carry data about a specific location related to an\nevent.\n\nThis geolocation information can be derived from techniques such as Geo IP,\nor be user-supplied.', - type: 'group', - fields: [ - { - name: 'city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - ], - }, - { - name: 'group', - title: 'Group', - group: 2, - description: - 'The group fields are meant to represent groups that are relevant\nto the event.', - type: 'group', - fields: [ - { - name: 'domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - ], - }, - { - name: 'hash', - title: 'Hash', - group: 2, - description: - 'The hash fields represent different hash algorithms and their values.\n\nField names for common hashes (e.g. MD5, SHA1) are predefined. Add fields for\nother hashes by lowercasing the hash algorithm name and using underscore separators\nas appropriate (snake case, e.g. sha3_512).', - type: 'group', - fields: [ - { - name: 'md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - }, - { - name: 'sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - }, - { - name: 'sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - }, - { - name: 'sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - }, - ], - }, - { - name: 'host', - title: 'Host', - group: 2, - description: - 'A host is defined as a general computing instance.\n\nECS host.* fields should be populated with details about the host on which the\nevent happened, or from which the measurement was taken. Host types include\nhardware, virtual machines, Docker containers, and Kubernetes nodes.', - type: 'group', - fields: [ - { - name: 'architecture', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system architecture.', - example: 'x86_64', - }, - { - name: 'domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the domain of which the host is a member.\n\nFor example, on Windows this could be the host Active Directory domain\nor NetBIOS domain name. For Linux this could be the domain of the host\nLDAP provider.', - example: 'CONTOSO', - default_field: false, - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'hostname', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Hostname of the host.\n\nIt normally contains what the `hostname` command returns on the host machine.', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique host id.\n\nAs hostname is not always unique, use values that are meaningful in your environment.\n\nExample: The current usage of `beat.name`.', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: 'Host ip addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Host mac addresses.', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.', - }, - { - name: 'os.family', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'os.full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'os.kernel', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'os.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'os.platform', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'os.version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Type of host.\n\nFor Cloud providers this can be the machine type like `t2.medium`. If vm,\nthis could be the container, for example, or other information meaningful\nin your environment.', - }, - { - name: 'uptime', - level: 'extended', - type: 'long', - description: 'Seconds the host has been up.', - example: 1325, - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'http', - title: 'HTTP', - group: 2, - description: - 'Fields related to HTTP activity. Use the `url` field set to store\nthe url of the request.', - type: 'group', - fields: [ - { - name: 'request.body.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Size in bytes of the request body.', - example: 887, - }, - { - name: 'request.body.content', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'The full HTTP request body.', - example: 'Hello world', - }, - { - name: 'request.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Total size in bytes of the request (body and headers).', - example: 1437, - }, - { - name: 'request.method', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'HTTP request method.\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'get, post, put', - }, - { - name: 'request.referrer', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Referrer for this HTTP request.', - example: 'https://blog.example.com/', - }, - { - name: 'response.body.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Size in bytes of the response body.', - example: 887, - }, - { - name: 'response.body.content', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'The full HTTP response body.', - example: 'Hello world', - }, - { - name: 'response.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Total size in bytes of the response (body and headers).', - example: 1437, - }, - { - name: 'response.status_code', - level: 'extended', - type: 'long', - format: 'string', - description: 'HTTP response status code.', - example: 404, - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'HTTP version.', - example: 1.1, - }, - ], - }, - { - name: 'interface', - title: 'Interface', - group: 2, - description: - 'The interface fields are used to record ingress and egress interface\ninformation when reported by an observer (e.g. firewall, router, load balancer)\nin the context of the observer handling a network connection. In the case of\na single observer interface (e.g. network sensor on a span port) only the observer.ingress\ninformation should be populated.', - type: 'group', - fields: [ - { - name: 'alias', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Interface alias as reported by the system, typically used in firewall\nimplementations for e.g. inside, outside, or dmz logical interface naming.', - example: 'outside', - default_field: false, - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface ID as reported by an observer (typically SNMP interface\nID).', - example: 10, - default_field: false, - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface name as reported by the system.', - example: 'eth0', - default_field: false, - }, - ], - }, - { - name: 'log', - title: 'Log', - group: 2, - description: - 'Details about the event logging mechanism or logging transport.\n\nThe log.* fields are typically populated with details about the logging mechanism\nused to create and/or transport the event. For example, syslog details belong\nunder `log.syslog.*`.\n\nThe details specific to your event source are typically not logged under `log.*`,\nbut rather in `event.*` or in other ECS fields.', - type: 'group', - fields: [ - { - name: 'file.path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - "Full path to the log file this event came from, including the\nfile name. It should include the drive letter, when appropriate.\n\nIf the event wasn't read from a log file, do not populate this field.", - example: '/var/log/fun-times.log', - default_field: false, - }, - { - name: 'level', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Original log level of the log event.\n\nIf the source of the event provides a log level or textual severity, this\nis the one that goes in `log.level`. If your source does not specify one,\nyou may put your event transport severity here (e.g. Syslog severity).\n\nSome examples are `warn`, `err`, `i`, `informational`.', - example: 'error', - }, - { - name: 'logger', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'The name of the logger inside an application. This is usually the\nname of the class which initialized the logger, or can be a custom name.', - example: 'org.elasticsearch.bootstrap.Bootstrap', - }, - { - name: 'origin.file.line', - level: 'extended', - type: 'integer', - description: - 'The line number of the file containing the source code which originated\nthe log event.', - example: 42, - }, - { - name: 'origin.file.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The name of the file containing the source code which originated\nthe log event.\n\nNote that this field is not meant to capture the log file. The correct field\nto capture the log file is `log.file.path`.', - example: 'Bootstrap.java', - }, - { - name: 'origin.function', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The name of the function or method which originated the log event.', - example: 'init', - }, - { - name: 'original', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is the original log message and contains the full log message\nbefore splitting it up in multiple parts.\n\nIn contrast to the `message` field which can contain an extracted part of\nthe log message, this field contains the original, full log message. It can\nhave already some modifications applied like encoding or new lines removed\nto clean up the log message.\n\nThis field is not indexed and doc_values are disabled so it cannot be queried\nbut the value can be retrieved from `_source`.', - example: 'Sep 19 08:26:10 localhost My log', - }, - { - name: 'syslog', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'The Syslog metadata of the event, if the event was transmitted\nvia Syslog. Please see RFCs 5424 or 3164.', - }, - { - name: 'syslog.facility.code', - level: 'extended', - type: 'long', - format: 'string', - description: - 'The Syslog numeric facility of the log event, if available.\n\nAccording to RFCs 5424 and 3164, this value should be an integer between 0\nand 23.', - example: 23, - }, - { - name: 'syslog.facility.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The Syslog text-based facility of the log event, if available.', - example: 'local7', - }, - { - name: 'syslog.priority', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Syslog numeric priority of the event, if available.\n\nAccording to RFCs 5424 and 3164, the priority is 8 * facility + severity.\nThis number is therefore expected to contain a value between 0 and 191.', - example: 135, - }, - { - name: 'syslog.severity.code', - level: 'extended', - type: 'long', - description: - 'The Syslog numeric severity of the log event, if available.\n\nIf the event source publishing via Syslog provides a different numeric severity\nvalue (e.g. firewall, IDS), your source numeric severity should go to `event.severity`.\nIf the event source does not specify a distinct severity, you can optionally\ncopy the Syslog severity to `event.severity`.', - example: 3, - }, - { - name: 'syslog.severity.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The Syslog numeric severity of the log event, if available.\n\nIf the event source publishing via Syslog provides a different severity value\n(e.g. firewall, IDS), your source text severity should go to `log.level`.\nIf the event source does not specify a distinct severity, you can optionally\ncopy the Syslog severity to `log.level`.', - example: 'Error', - }, - ], - }, - { - name: 'network', - title: 'Network', - group: 2, - description: - 'The network is defined as the communication path over which a host\nor network event happens.\n\nThe network.* fields should be populated with details about the network activity\nassociated with an event.', - type: 'group', - fields: [ - { - name: 'application', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A name given to an application level protocol. This can be arbitrarily\nassigned for things like microservices, but also apply to things like skype,\nicq, facebook, twitter. This would be used in situations where the vendor\nor service can be decoded such as from the source/dest IP owners, ports, or\nwire format.\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'aim', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: - 'Total bytes transferred in both directions.\n\nIf `source.bytes` and `destination.bytes` are known, `network.bytes` is their\nsum.', - example: 368, - }, - { - name: 'community_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A hash of source and destination IPs and ports, as well as the\nprotocol used in a communication. This is a tool-agnostic standard to identify\nflows.\n\nLearn more at https://github.com/corelight/community-id-spec.', - example: '1:hO+sN4H+MG5MY/8hIrXPqc4ZQz0=', - }, - { - name: 'direction', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - "Direction of the network traffic.\nRecommended values are:\n * inbound\n * outbound\n * internal\n * external\n * unknown\n\nWhen mapping events from a host-based monitoring context, populate this field from the host's point of view.\nWhen mapping events from a network or perimeter-based monitoring context, populate this field from the point of view of your network perimeter.", - example: 'inbound', - }, - { - name: 'forwarded_ip', - level: 'core', - type: 'ip', - description: 'Host IP address when the source IP address is the proxy.', - example: '192.1.1.2', - }, - { - name: 'iana_number', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'IANA Protocol Number (https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml).\nStandardized list of protocols. This aligns well with NetFlow and sFlow related\nlogs which use the IANA Protocol Number.', - example: 6, - }, - { - name: 'inner', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'Network.inner fields are added in addition to network.vlan fields\nto describe the innermost VLAN when q-in-q VLAN tagging is present. Allowed\nfields include vlan.id and vlan.name. Inner vlan fields are typically used\nwhen sending traffic with multiple 802.1q encapsulations to a network sensor\n(e.g. Zeek, Wireshark.)', - default_field: false, - }, - { - name: 'inner.vlan.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'inner.vlan.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name given by operators to sections of their network.', - example: 'Guest Wifi', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: - 'Total packets transferred in both directions.\n\nIf `source.packets` and `destination.packets` are known, `network.packets`\nis their sum.', - example: 24, - }, - { - name: 'protocol', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'L7 Network protocol name. ex. http, lumberjack, transport protocol.\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'http', - }, - { - name: 'transport', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Same as network.iana_number, but instead using the Keyword name\nof the transport layer (udp, tcp, ipv6-icmp, etc.)\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'tcp', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'In the OSI Model this would be the Network Layer. ipv4, ipv6,\nipsec, pim, etc\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'ipv4', - }, - { - name: 'vlan.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'vlan.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - ], - }, - { - name: 'observer', - title: 'Observer', - group: 2, - description: - 'An observer is defined as a special network, security, or application\ndevice used to detect, observe, or create network, security, or application-related\nevents and metrics.\n\nThis could be a custom hardware appliance or a server that has been configured\nto run special network, security, or application software. Examples include\nfirewalls, web proxies, intrusion detection/prevention systems, network monitoring\nsensors, web application firewalls, data loss prevention systems, and APM servers.\nThe observer.* fields shall be populated with details of the system, if any,\nthat detects, observes and/or creates a network, security, or application event\nor metric. Message queues and ETL components used in processing events or metrics\nare not considered observers in ECS.', - type: 'group', - fields: [ - { - name: 'egress', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'Observer.egress holds information like interface number and name,\nvlan, and zone information to classify egress traffic. Single armed monitoring\nsuch as a network sensor on a span port should only use observer.ingress\nto categorize traffic.', - default_field: false, - }, - { - name: 'egress.interface.alias', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Interface alias as reported by the system, typically used in firewall\nimplementations for e.g. inside, outside, or dmz logical interface naming.', - example: 'outside', - default_field: false, - }, - { - name: 'egress.interface.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface ID as reported by an observer (typically SNMP interface\nID).', - example: 10, - default_field: false, - }, - { - name: 'egress.interface.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface name as reported by the system.', - example: 'eth0', - default_field: false, - }, - { - name: 'egress.vlan.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'egress.vlan.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - { - name: 'egress.zone', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Network zone of outbound traffic as reported by the observer to\ncategorize the destination area of egress traffic, e.g. Internal, External,\nDMZ, HR, Legal, etc.', - example: 'Public_Internet', - default_field: false, - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'hostname', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Hostname of the observer.', - }, - { - name: 'ingress', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'Observer.ingress holds information like interface number and name,\nvlan, and zone information to classify ingress traffic. Single armed monitoring\nsuch as a network sensor on a span port should only use observer.ingress\nto categorize traffic.', - default_field: false, - }, - { - name: 'ingress.interface.alias', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Interface alias as reported by the system, typically used in firewall\nimplementations for e.g. inside, outside, or dmz logical interface naming.', - example: 'outside', - default_field: false, - }, - { - name: 'ingress.interface.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface ID as reported by an observer (typically SNMP interface\nID).', - example: 10, - default_field: false, - }, - { - name: 'ingress.interface.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface name as reported by the system.', - example: 'eth0', - default_field: false, - }, - { - name: 'ingress.vlan.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'ingress.vlan.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - { - name: 'ingress.zone', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Network zone of incoming traffic as reported by the observer to\ncategorize the source area of ingress traffic. e.g. internal, External, DMZ,\nHR, Legal, etc.', - example: 'DMZ', - default_field: false, - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: 'IP addresses of the observer.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC addresses of the observer', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Custom name of the observer.\n\nThis is a name that can be given to an observer. This can be helpful for example\nif multiple firewalls of the same model are used in an organization.\n\nIf no custom name is needed, the field can be left empty.', - example: '1_proxySG', - }, - { - name: 'os.family', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'os.full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'os.kernel', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'os.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'os.platform', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'os.version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - { - name: 'product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The product name of the observer.', - example: 's200', - }, - { - name: 'serial_number', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Observer serial number.', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of the observer the data is coming from.\n\nThere is no predefined list of observer types. Some examples are `forwarder`,\n`firewall`, `ids`, `ips`, `proxy`, `poller`, `sensor`, `APM server`.', - example: 'firewall', - }, - { - name: 'vendor', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Vendor name of the observer.', - example: 'Symantec', - }, - { - name: 'version', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Observer version.', - }, - ], - }, - { - name: 'organization', - title: 'Organization', - group: 2, - description: - 'The organization fields enrich data with information about the company\nor entity the data is associated with.\n\nThese fields help you arrange or filter data stored in an index by one or multiple\norganizations.', - type: 'group', - fields: [ - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the organization.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - }, - ], - }, - { - name: 'os', - title: 'Operating System', - group: 2, - description: 'The OS fields contain information about the operating system.', - type: 'group', - fields: [ - { - name: 'family', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'kernel', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'platform', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - ], - }, - { - name: 'package', - title: 'Package', - group: 2, - description: - 'These fields contain information about an installed software package.\nIt contains general information about a package, such as name, version or size.\nIt also contains installation details, such as time or location.', - type: 'group', - fields: [ - { - name: 'architecture', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Package architecture.', - example: 'x86_64', - }, - { - name: 'build_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the build version of the installed\npackage.\n\nFor example use the commit SHA of a non-released package.', - example: '36f4f7e89dd61b0988b12ee000b98966867710cd', - default_field: false, - }, - { - name: 'checksum', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Checksum of the installed package for verification.', - example: '68b329da9893e34099c7d8ad5cb9c940', - }, - { - name: 'description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Description of the package.', - example: - 'Open source programming language to build simple/reliable/efficient\nsoftware.', - }, - { - name: 'install_scope', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Indicating how the package was installed, e.g. user-local, global.', - example: 'global', - }, - { - name: 'installed', - level: 'extended', - type: 'date', - description: 'Time when package was installed.', - }, - { - name: 'license', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'License under which the package was released.\n\nUse a short name, e.g. the license identifier from SPDX License List where\npossible (https://spdx.org/licenses/).', - example: 'Apache License 2.0', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Package name', - example: 'go', - }, - { - name: 'path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Path where the package is installed.', - example: '/usr/local/Cellar/go/1.12.9/', - }, - { - name: 'reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Home page or reference URL of the software in this package, if\navailable.', - example: 'https://golang.org', - default_field: false, - }, - { - name: 'size', - level: 'extended', - type: 'long', - format: 'string', - description: 'Package size in bytes.', - example: 62231, - }, - { - name: 'type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Type of package.\n\nThis should contain the package file type, rather than the package manager\nname. Examples: rpm, dpkg, brew, npm, gem, nupkg, jar.', - example: 'rpm', - default_field: false, - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Package version', - example: '1.12.9', - }, - ], - }, - { - name: 'pe', - title: 'PE Header', - group: 2, - description: 'These fields contain Windows Portable Executable (PE) metadata.', - type: 'group', - fields: [ - { - name: 'architecture', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'CPU architecture target for the file.', - example: 'x64', - default_field: false, - }, - { - name: 'company', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal company name of the file, provided at compile-time.', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal description of the file, provided at compile-time.', - example: 'Paint', - default_field: false, - }, - { - name: 'file_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal version of the file, provided at compile-time.', - example: '6.3.9600.17415', - default_field: false, - }, - { - name: 'imphash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A hash of the imports in a PE file. An imphash -- or import hash\n-- can be used to fingerprint binaries even after recompilation or other code-level\ntransformations have occurred, which would change more traditional hash values.\n\nLearn more at https://www.fireeye.com/blog/threat-research/2014/01/tracking-malware-import-hashing.html.', - example: '0c6803c4e922103c4dca5963aad36ddf', - default_field: false, - }, - { - name: 'original_file_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal name of the file, provided at compile-time.', - example: 'MSPAINT.EXE', - default_field: false, - }, - { - name: 'product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal product name of the file, provided at compile-time.', - example: 'Microsoft® Windows® Operating System', - default_field: false, - }, - ], - }, - { - name: 'process', - title: 'Process', - group: 2, - description: - 'These fields contain information about a process.\n\nThese fields can help you correlate metrics information with a process id/name\nfrom a log message. The `process.pid` often stays in the metric itself and\nis copied to the global field for correlation.', - type: 'group', - fields: [ - { - name: 'args', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.', - example: ['/usr/bin/ssh', '-l', 'user', '10.0.0.16'], - }, - { - name: 'args_count', - level: 'extended', - type: 'long', - description: - 'Length of the process.args array.\n\nThis field can be useful for querying or performing bucket analysis on how\nmany arguments were provided to start a process. More arguments may be an\nindication of suspicious activity.', - example: 4, - default_field: false, - }, - { - name: 'code_signature.exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'code_signature.subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'code_signature.trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - { - name: 'command_line', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: - 'Full command line that started the process, including the absolute\npath to the executable, and all arguments.\n\nSome arguments may be filtered to protect sensitive information.', - example: '/usr/bin/ssh -l user 10.0.0.16', - default_field: false, - }, - { - name: 'entity_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier for the process.\n\nThe implementation of this is specified by the data source, but some examples\nof what could be used here are a process-generated UUID, Sysmon Process GUIDs,\nor a hash of some uniquely identifying components of a process.\n\nConstructing a globally unique identifier is a common practice to mitigate\nPID reuse as well as to identify a specific process over time, across multiple\nmonitored hosts.', - example: 'c2c455d9f99375d', - default_field: false, - }, - { - name: 'executable', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Absolute path to the process executable.', - example: '/usr/bin/ssh', - }, - { - name: 'exit_code', - level: 'extended', - type: 'long', - description: - 'The exit code of the process, if this is a termination event.\n\nThe field should be absent if there is no exit code for the event (e.g. process\nstart).', - example: 137, - default_field: false, - }, - { - name: 'hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - }, - { - name: 'hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - }, - { - name: 'hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - }, - { - name: 'hash.sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Process name.\n\nSometimes called program name or similar.', - example: 'ssh', - }, - { - name: 'parent.args', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of process arguments.\n\nMay be filtered to protect sensitive information.', - example: ['ssh', '-l', 'user', '10.0.0.16'], - default_field: false, - }, - { - name: 'parent.args_count', - level: 'extended', - type: 'long', - description: - 'Length of the process.args array.\n\nThis field can be useful for querying or performing bucket analysis on how\nmany arguments were provided to start a process. More arguments may be an\nindication of suspicious activity.', - example: 4, - default_field: false, - }, - { - name: 'parent.code_signature.exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'parent.code_signature.status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'parent.code_signature.subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'parent.code_signature.trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'parent.code_signature.valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - { - name: 'parent.command_line', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: - 'Full command line that started the process, including the absolute\npath to the executable, and all arguments.\n\nSome arguments may be filtered to protect sensitive information.', - example: '/usr/bin/ssh -l user 10.0.0.16', - default_field: false, - }, - { - name: 'parent.entity_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier for the process.\n\nThe implementation of this is specified by the data source, but some examples\nof what could be used here are a process-generated UUID, Sysmon Process GUIDs,\nor a hash of some uniquely identifying components of a process.\n\nConstructing a globally unique identifier is a common practice to mitigate\nPID reuse as well as to identify a specific process over time, across multiple\nmonitored hosts.', - example: 'c2c455d9f99375d', - default_field: false, - }, - { - name: 'parent.executable', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: 'Absolute path to the process executable.', - example: '/usr/bin/ssh', - default_field: false, - }, - { - name: 'parent.exit_code', - level: 'extended', - type: 'long', - description: - 'The exit code of the process, if this is a termination event.\n\nThe field should be absent if there is no exit code for the event (e.g. process\nstart).', - example: 137, - default_field: false, - }, - { - name: 'parent.hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - default_field: false, - }, - { - name: 'parent.hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - default_field: false, - }, - { - name: 'parent.hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - default_field: false, - }, - { - name: 'parent.hash.sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - default_field: false, - }, - { - name: 'parent.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: 'Process name.\n\nSometimes called program name or similar.', - example: 'ssh', - default_field: false, - }, - { - name: 'parent.pgid', - level: 'extended', - type: 'long', - format: 'string', - description: 'Identifier of the group of processes the process belongs to.', - default_field: false, - }, - { - name: 'parent.pid', - level: 'core', - type: 'long', - format: 'string', - description: 'Process id.', - example: 4242, - default_field: false, - }, - { - name: 'parent.ppid', - level: 'extended', - type: 'long', - format: 'string', - description: "Parent process' pid.", - example: 4241, - default_field: false, - }, - { - name: 'parent.start', - level: 'extended', - type: 'date', - description: 'The time the process started.', - example: '2016-05-23T08:05:34.853Z', - default_field: false, - }, - { - name: 'parent.thread.id', - level: 'extended', - type: 'long', - format: 'string', - description: 'Thread ID.', - example: 4242, - default_field: false, - }, - { - name: 'parent.thread.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Thread name.', - example: 'thread-0', - default_field: false, - }, - { - name: 'parent.title', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: - 'Process title.\n\nThe proctitle, some times the same as process name. Can also be different:\nfor example a browser setting its title to the web page currently opened.', - default_field: false, - }, - { - name: 'parent.uptime', - level: 'extended', - type: 'long', - description: 'Seconds the process has been up.', - example: 1325, - default_field: false, - }, - { - name: 'parent.working_directory', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: 'The working directory of the process.', - example: '/home/alice', - default_field: false, - }, - { - name: 'pe.architecture', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'CPU architecture target for the file.', - example: 'x64', - default_field: false, - }, - { - name: 'pe.company', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal company name of the file, provided at compile-time.', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'pe.description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal description of the file, provided at compile-time.', - example: 'Paint', - default_field: false, - }, - { - name: 'pe.file_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal version of the file, provided at compile-time.', - example: '6.3.9600.17415', - default_field: false, - }, - { - name: 'pe.imphash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A hash of the imports in a PE file. An imphash -- or import hash\n-- can be used to fingerprint binaries even after recompilation or other code-level\ntransformations have occurred, which would change more traditional hash values.\n\nLearn more at https://www.fireeye.com/blog/threat-research/2014/01/tracking-malware-import-hashing.html.', - example: '0c6803c4e922103c4dca5963aad36ddf', - default_field: false, - }, - { - name: 'pe.original_file_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal name of the file, provided at compile-time.', - example: 'MSPAINT.EXE', - default_field: false, - }, - { - name: 'pe.product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal product name of the file, provided at compile-time.', - example: 'Microsoft® Windows® Operating System', - default_field: false, - }, - { - name: 'pgid', - level: 'extended', - type: 'long', - format: 'string', - description: 'Identifier of the group of processes the process belongs to.', - }, - { - name: 'pid', - level: 'core', - type: 'long', - format: 'string', - description: 'Process id.', - example: 4242, - }, - { - name: 'ppid', - level: 'extended', - type: 'long', - format: 'string', - description: "Parent process' pid.", - example: 4241, - }, - { - name: 'start', - level: 'extended', - type: 'date', - description: 'The time the process started.', - example: '2016-05-23T08:05:34.853Z', - }, - { - name: 'thread.id', - level: 'extended', - type: 'long', - format: 'string', - description: 'Thread ID.', - example: 4242, - }, - { - name: 'thread.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Thread name.', - example: 'thread-0', - }, - { - name: 'title', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'Process title.\n\nThe proctitle, some times the same as process name. Can also be different:\nfor example a browser setting its title to the web page currently opened.', - }, - { - name: 'uptime', - level: 'extended', - type: 'long', - description: 'Seconds the process has been up.', - example: 1325, - }, - { - name: 'working_directory', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'The working directory of the process.', - example: '/home/alice', - }, - ], - }, - { - name: 'registry', - title: 'Registry', - group: 2, - description: 'Fields related to Windows Registry operations.', - type: 'group', - fields: [ - { - name: 'data.bytes', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Original bytes written with base64 encoding.\n\nFor Windows registry operations, such as SetValueEx and RegQueryValueEx, this\ncorresponds to the data pointed by `lp_data`. This is optional but provides\nbetter recoverability and should be populated for REG_BINARY encoded values.', - example: 'ZQBuAC0AVQBTAAAAZQBuAAAAAAA=', - default_field: false, - }, - { - name: 'data.strings', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Content when writing string types.\n\nPopulated as an array when writing string data to the registry. For single\nstring registry types (REG_SZ, REG_EXPAND_SZ), this should be an array with\none string. For sequences of string with REG_MULTI_SZ, this array will be\nvariable length. For numeric data, such as REG_DWORD and REG_QWORD, this should\nbe populated with the decimal representation (e.g `"1"`).', - example: '["C:\\rta\\red_ttp\\bin\\myapp.exe"]', - default_field: false, - }, - { - name: 'data.type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Standard registry type for encoding contents', - example: 'REG_SZ', - default_field: false, - }, - { - name: 'hive', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Abbreviated name for the hive.', - example: 'HKLM', - default_field: false, - }, - { - name: 'key', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Hive-relative path of keys.', - example: - 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\winword.exe', - default_field: false, - }, - { - name: 'path', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Full path, including hive, key and value', - example: - 'HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution\nOptions\\winword.exe\\Debugger', - default_field: false, - }, - { - name: 'value', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the value written.', - example: 'Debugger', - default_field: false, - }, - ], - }, - { - name: 'related', - title: 'Related', - group: 2, - description: - 'This field set is meant to facilitate pivoting around a piece of\ndata.\n\nSome pieces of information can be seen in many places in an ECS event. To facilitate\nsearching for them, store an array of all seen values to their corresponding\nfield in `related.`.\n\nA concrete example is IP addresses, which can be under host, observer, source,\ndestination, client, server, and network.forwarded_ip. If you append all IPs\nto `related.ip`, you can then search for a given IP trivially, no matter where\nit appeared, by querying `related.ip:192.0.2.15`.', - type: 'group', - fields: [ - { - name: 'hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - "All the hashes seen on your event. Populating this field, then\nusing it to search for hashes can help in situations where you're unsure what\nthe hash algorithm is (and therefore which key name to search).", - default_field: false, - }, - { - name: 'ip', - level: 'extended', - type: 'ip', - description: 'All of the IPs seen on your event.', - }, - { - name: 'user', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'All the user names seen on your event.', - default_field: false, - }, - ], - }, - { - name: 'rule', - title: 'Rule', - group: 2, - description: - 'Rule fields are used to capture the specifics of any observer or\nagent rules that generate alerts or other notable events.\n\nExamples of data sources that would populate the rule fields include: network\nadmission control platforms, network or host IDS/IPS, network firewalls, web\napplication firewalls, url filters, endpoint detection and response (EDR) systems,\netc.', - type: 'group', - fields: [ - { - name: 'author', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name, organization, or pseudonym of the author or authors who created\nthe rule used to generate this event.', - example: ['Star-Lord'], - default_field: false, - }, - { - name: 'category', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A categorization value keyword used by the entity using the rule\nfor detection of this event.', - example: 'Attempted Information Leak', - default_field: false, - }, - { - name: 'description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The description of the rule generating the event.', - example: 'Block requests to public DNS over HTTPS / TLS protocols', - default_field: false, - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A rule ID that is unique within the scope of an agent, observer,\nor other entity using the rule for detection of this event.', - example: 101, - default_field: false, - }, - { - name: 'license', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the license under which the rule used to generate this\nevent is made available.', - example: 'Apache 2.0', - default_field: false, - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The name of the rule or signature generating the event.', - example: 'BLOCK_DNS_over_TLS', - default_field: false, - }, - { - name: 'reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Reference URL to additional information about the rule used to\ngenerate this event.\n\nThe URL can point to the vendor documentation about the rule. If that is\nnot available, it can also be a link to a more general page describing this\ntype of alert.', - example: 'https://en.wikipedia.org/wiki/DNS_over_TLS', - default_field: false, - }, - { - name: 'ruleset', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the ruleset, policy, group, or parent category in which\nthe rule used to generate this event is a member.', - example: 'Standard_Protocol_Filters', - default_field: false, - }, - { - name: 'uuid', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A rule ID that is unique within the scope of a set or group of\nagents, observers, or other entities using the rule for detection of this\nevent.', - example: 1100110011, - default_field: false, - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The version / revision of the rule being used for analysis.', - example: 1.1, - default_field: false, - }, - ], - }, - { - name: 'search', - title: 'Search', - group: 2, - description: - 'The Search fields describe information about a search request event:\nquery or pagination. The fields that should be used with this field set include:\n`event.action` to describe the search action (e.g. `search.query`, `search.page`,\netc.), `event.duration` to describe the duration of a search request, `@timestamp`\nto record the event original timestamp and optionally the `source` fields\nto record context information such as `user.id` or `geo`.', - type: 'group', - fields: [ - { - name: 'query.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'An opaque query identifier. This identifier needs to be unique\nto a user query, and all subsequent events (pagination, clicks) need to have\nthe same query identifier.', - example: '2dc15175-de0d-44db-86d8-8a99f41b7a11', - default_field: false, - }, - { - name: 'query.page', - level: 'extended', - type: 'long', - description: - 'For search results that support pagination, this represents the\ncurrent page being requested. Initial search requests are `1` while subsequent\npage requests are incremental.', - example: 1, - default_field: false, - }, - { - name: 'query.value', - level: 'extended', - type: 'keyword', - ignore_above: 4096, - description: - 'The query string being searched on. This field is not analyzed\nand should not be pre-processed in any way in the event (e.g. normalization\nlist lowercasing). This is useful for search use-cases that use a one- box\nstyle search interface. Other interfaces will have to rely on additional custom\nfields or labels to represent things like filters applied, extra parameters,\nuser context, etc.', - example: 'where does the rain in Spain mainly fall', - default_field: false, - }, - { - name: 'results.ids', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - "A list of opaque document IDs representing the results that were\nshown to the user. This is effectively the impression list and it's size should\nbe equal to `results.size`. This field can be empty when there are no results\nto return.", - example: ['user:82375akja9f', 'issue:2782630'], - default_field: false, - }, - { - name: 'results.size', - level: 'extended', - type: 'long', - description: - 'The size of the result set displayed to the user. This should be\nequivalent to the length of the results in `results.ids`. This is also known\nas the page size or limit.', - example: 10, - default_field: false, - }, - { - name: 'results.total', - level: 'extended', - type: 'long', - description: - 'The total number of matches for this query. This number is always\ngreater than or equal to `results.size`. This is the `hits.total` field in\nthe query response.', - example: 134509, - default_field: false, - }, - ], - }, - { - name: 'server', - title: 'Server', - group: 2, - description: - 'A Server is defined as the responder in a network connection for\nevents regarding sessions, connections, or bidirectional flow records.\n\nFor TCP events, the server is the receiver of the initial SYN packet(s) of the\nTCP connection. For other protocols, the server is generally the responder in\nthe network transaction. Some systems actually use the term "responder" to refer\nthe server in TCP connections. The server fields describe details about the\nsystem acting as the server in the network event. Server fields are usually\npopulated in conjunction with client fields. Server fields are generally not\npopulated for packet-level events.\n\nClient / server representations can add semantic context to an exchange, which\nis helpful to visualize the data in certain situations. If your context falls\nin that category, you should still ensure that source and destination are filled\nappropriately.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Some event server addresses are defined ambiguously. The event\nwill sometimes list an IP, a domain or a unix socket. You should always store\nthe raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'as.number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'as.organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the server to the client.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Server domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: 'IP address of the server (IPv4 or IPv6).', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC address of the server.', - }, - { - name: 'nat.ip', - level: 'extended', - type: 'ip', - description: - 'Translated ip of destination based NAT sessions (e.g. internet\nto private DMZ)\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'nat.port', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Translated port of destination based NAT sessions (e.g. internet\nto private DMZ)\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the server to the client.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the server.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered server domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'service', - title: 'Service', - group: 2, - description: - 'The service fields describe the service for or from which the data\nwas collected.\n\nThese fields help you find and correlate logs for a specific service and version.', - type: 'group', - fields: [ - { - name: 'ephemeral_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Ephemeral identifier of this service (if one exists).\n\nThis id normally changes across restarts, but `service.id` does not.', - example: '8a4f500f', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier of the running service. If the service is comprised\nof many nodes, the `service.id` should be the same for all nodes.\n\nThis id should uniquely identify the service. This makes it possible to correlate\nlogs and metrics for one specific service, no matter which particular node\nemitted the event.\n\nNote that if you need to see the events from one specific host of the service,\nyou should filter on that `host.name` or `host.id` instead.', - example: 'd37e5ebfe0ae6c4972dbe9f0174a1637bb8247f6', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the service data is collected from.\n\nThe name of the service is normally user given. This allows for distributed\nservices that run on multiple hosts to correlate the related instances based\non the name.\n\nIn the case of Elasticsearch the `service.name` could contain the cluster\nname. For Beats the `service.name` is by default a copy of the `service.type`\nfield if no name is specified.', - example: 'elasticsearch-metrics', - }, - { - name: 'node.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of a service node.\n\nThis allows for two nodes of the same service running on the same host to\nbe differentiated. Therefore, `service.node.name` should typically be unique\nacross nodes of a given service.\n\nIn the case of Elasticsearch, the `service.node.name` could contain the unique\nnode name within the Elasticsearch cluster. In cases where the service does not\nhave the concept of a node name, the host name or container name can be used\nto distinguish running instances that make up this service. If those do not\nprovide uniqueness (e.g. multiple instances of the service running on the\nsame host) - the node name can be manually set.', - example: 'instance-0000000016', - }, - { - name: 'state', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Current state of the service.', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of the service data is collected from.\n\nThe type can be used to group and correlate logs and metrics from one service\ntype.\n\nExample: If logs or metrics are collected from Elasticsearch, `service.type`\nwould be `elasticsearch`.', - example: 'elasticsearch', - }, - { - name: 'version', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Version of the service the data was collected from.\n\nThis allows to look at a data set only for a specific version of a service.', - example: '3.2.4', - }, - ], - }, - { - name: 'source', - title: 'Source', - group: 2, - description: - 'Source fields describe details about the source of a packet/event.\n\nSource fields are usually populated in conjunction with destination fields.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Some event source addresses are defined ambiguously. The event\nwill sometimes list an IP, a domain or a unix socket. You should always store\nthe raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'as.number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'as.organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the source to the destination.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Source domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: 'IP address of the source (IPv4 or IPv6).', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC address of the source.', - }, - { - name: 'nat.ip', - level: 'extended', - type: 'ip', - description: - 'Translated ip of source based NAT sessions (e.g. internal client\nto internet)\n\nTypically connections traversing load balancers, firewalls, or routers.', - }, - { - name: 'nat.port', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Translated port of source based NAT sessions. (e.g. internal client\nto internet)\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the source to the destination.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the source.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered source domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'threat', - title: 'Threat', - group: 2, - description: - 'Fields to classify events and alerts according to a threat taxonomy\nsuch as the Mitre ATT&CK framework.\n\nThese fields are for users to classify alerts from all of their sources (e.g.\nIDS, NGFW, etc.) within a common taxonomy. The threat.tactic.* are meant to\ncapture the high level category of the threat (e.g. "impact"). The threat.technique.*\nfields are meant to capture which kind of approach is used by this detected\nthreat, to accomplish the goal (e.g. "endpoint denial of service").', - type: 'group', - fields: [ - { - name: 'framework', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the threat framework used to further categorize and classify\nthe tactic and technique of the reported threat. Framework classification\ncan be provided by detecting systems, evaluated at ingest time, or retrospectively\ntagged to events.', - example: 'MITRE ATT&CK', - }, - { - name: 'tactic.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The id of tactic used by this threat. You can use the Mitre ATT&CK\nMatrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/\n)', - example: 'TA0040', - }, - { - name: 'tactic.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the type of tactic used by this threat. You can use the\nMitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/\n)', - example: 'impact', - }, - { - name: 'tactic.reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The reference url of tactic used by this threat. You can use the\nMitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/\n)', - example: 'https://attack.mitre.org/tactics/TA0040/', - }, - { - name: 'technique.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The id of technique used by this tactic. You can use the Mitre\nATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/\n)', - example: 'T1499', - }, - { - name: 'technique.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'The name of technique used by this tactic. You can use the Mitre\nATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/\n)', - example: 'endpoint denial of service', - }, - { - name: 'technique.reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The reference url of technique used by this tactic. You can use\nthe Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/\n)', - example: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - { - name: 'tls', - title: 'TLS', - group: 2, - description: - 'Fields related to a TLS connection. These fields focus on the TLS\nprotocol itself and intentionally avoids in-depth analysis of the related x.509\ncertificate files.', - type: 'group', - fields: [ - { - name: 'cipher', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'String indicating the cipher used during the current connection.', - example: 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256', - default_field: false, - }, - { - name: 'client.certificate', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'PEM-encoded stand-alone certificate offered by the client. This\nis usually mutually-exclusive of `client.certificate_chain` since this value\nalso exists in that list.', - example: 'MII...', - default_field: false, - }, - { - name: 'client.certificate_chain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of PEM-encoded certificates that make up the certificate\nchain offered by the client. This is usually mutually-exclusive of `client.certificate`\nsince that value should be the first certificate in the chain.', - example: ['MII...', 'MII...'], - default_field: false, - }, - { - name: 'client.hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the MD5 digest of DER-encoded version\nof certificate offered by the client. For consistency with other hash values,\nthis value should be formatted as an uppercase hash.', - example: '0F76C7F2C55BFD7D8E8B8F4BFBF0C9EC', - default_field: false, - }, - { - name: 'client.hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the SHA1 digest of DER-encoded version\nof certificate offered by the client. For consistency with other hash values,\nthis value should be formatted as an uppercase hash.', - example: '9E393D93138888D288266C2D915214D1D1CCEB2A', - default_field: false, - }, - { - name: 'client.hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the SHA256 digest of DER-encoded\nversion of certificate offered by the client. For consistency with other hash\nvalues, this value should be formatted as an uppercase hash.', - example: '0687F666A054EF17A08E2F2162EAB4CBC0D265E1D7875BE74BF3C712CA92DAF0', - default_field: false, - }, - { - name: 'client.issuer', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Distinguished name of subject of the issuer of the x.509 certificate\npresented by the client.', - example: 'CN=MyDomain Root CA, OU=Infrastructure Team, DC=mydomain, DC=com', - default_field: false, - }, - { - name: 'client.ja3', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A hash that identifies clients based on how they perform an SSL/TLS\nhandshake.', - example: 'd4e5b18d6b55c71272893221c96ba240', - default_field: false, - }, - { - name: 'client.not_after', - level: 'extended', - type: 'date', - description: - 'Date/Time indicating when client certificate is no longer considered\nvalid.', - example: '2021-01-01T00:00:00.000Z', - default_field: false, - }, - { - name: 'client.not_before', - level: 'extended', - type: 'date', - description: 'Date/Time indicating when client certificate is first considered\nvalid.', - example: '1970-01-01T00:00:00.000Z', - default_field: false, - }, - { - name: 'client.server_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Also called an SNI, this tells the server which hostname to which\nthe client is attempting to connect. When this value is available, it should\nget copied to `destination.domain`.', - example: 'www.elastic.co', - default_field: false, - }, - { - name: 'client.subject', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Distinguished name of subject of the x.509 certificate presented\nby the client.', - example: 'CN=myclient, OU=Documentation Team, DC=mydomain, DC=com', - default_field: false, - }, - { - name: 'client.supported_ciphers', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Array of ciphers offered by the client during the client hello.', - example: [ - 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384', - 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384', - '...', - ], - default_field: false, - }, - { - name: 'curve', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'String indicating the curve used for the given cipher, when applicable.', - example: 'secp256r1', - default_field: false, - }, - { - name: 'established', - level: 'extended', - type: 'boolean', - description: - 'Boolean flag indicating if the TLS negotiation was successful and\ntransitioned to an encrypted tunnel.', - default_field: false, - }, - { - name: 'next_protocol', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'String indicating the protocol being tunneled. Per the values in\nthe IANA registry (https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids),\nthis string should be lower case.', - example: 'http/1.1', - default_field: false, - }, - { - name: 'resumed', - level: 'extended', - type: 'boolean', - description: - 'Boolean flag indicating if this TLS connection was resumed from\nan existing TLS negotiation.', - default_field: false, - }, - { - name: 'server.certificate', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'PEM-encoded stand-alone certificate offered by the server. This\nis usually mutually-exclusive of `server.certificate_chain` since this value\nalso exists in that list.', - example: 'MII...', - default_field: false, - }, - { - name: 'server.certificate_chain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of PEM-encoded certificates that make up the certificate\nchain offered by the server. This is usually mutually-exclusive of `server.certificate`\nsince that value should be the first certificate in the chain.', - example: ['MII...', 'MII...'], - default_field: false, - }, - { - name: 'server.hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the MD5 digest of DER-encoded version\nof certificate offered by the server. For consistency with other hash values,\nthis value should be formatted as an uppercase hash.', - example: '0F76C7F2C55BFD7D8E8B8F4BFBF0C9EC', - default_field: false, - }, - { - name: 'server.hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the SHA1 digest of DER-encoded version\nof certificate offered by the server. For consistency with other hash values,\nthis value should be formatted as an uppercase hash.', - example: '9E393D93138888D288266C2D915214D1D1CCEB2A', - default_field: false, - }, - { - name: 'server.hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the SHA256 digest of DER-encoded\nversion of certificate offered by the server. For consistency with other hash\nvalues, this value should be formatted as an uppercase hash.', - example: '0687F666A054EF17A08E2F2162EAB4CBC0D265E1D7875BE74BF3C712CA92DAF0', - default_field: false, - }, - { - name: 'server.issuer', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Subject of the issuer of the x.509 certificate presented by the\nserver.', - example: 'CN=MyDomain Root CA, OU=Infrastructure Team, DC=mydomain, DC=com', - default_field: false, - }, - { - name: 'server.ja3s', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A hash that identifies servers based on how they perform an SSL/TLS\nhandshake.', - example: '394441ab65754e2207b1e1b457b3641d', - default_field: false, - }, - { - name: 'server.not_after', - level: 'extended', - type: 'date', - description: - 'Timestamp indicating when server certificate is no longer considered\nvalid.', - example: '2021-01-01T00:00:00.000Z', - default_field: false, - }, - { - name: 'server.not_before', - level: 'extended', - type: 'date', - description: 'Timestamp indicating when server certificate is first considered\nvalid.', - example: '1970-01-01T00:00:00.000Z', - default_field: false, - }, - { - name: 'server.subject', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Subject of the x.509 certificate presented by the server.', - example: 'CN=www.mydomain.com, OU=Infrastructure Team, DC=mydomain, DC=com', - default_field: false, - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Numeric part of the version parsed from the original string.', - example: '1.2', - default_field: false, - }, - { - name: 'version_protocol', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Normalized lowercase protocol name parsed from original string.', - example: 'tls', - default_field: false, - }, - ], - }, - { - name: 'tracing', - title: 'Tracing', - group: 2, - description: - 'Distributed tracing makes it possible to analyze performance throughout\na microservice architecture all in one view. This is accomplished by tracing\nall of the requests - from the initial web request in the front-end service\n- to queries made through multiple back-end services.', - type: 'group', - fields: [ - { - name: 'trace.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier of the trace.\n\nA trace groups multiple events like transactions that belong together. For\nexample, a user request handled by multiple inter-connected services.', - example: '4bf92f3577b34da6a3ce929d0e0e4736', - }, - { - name: 'transaction.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier of the transaction.\n\nA transaction is the highest level of work measured within a service, such\nas a request to a server.', - example: '00f067aa0ba902b7', - }, - ], - }, - { - name: 'url', - title: 'URL', - group: 2, - description: - 'URL fields provide support for complete or partial URLs, and supports\nthe breaking down into scheme, domain, path, and so on.', - type: 'group', - fields: [ - { - name: 'domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Domain of the url, such as "www.elastic.co".\n\nIn some cases a URL may refer to an IP and/or port directly, without a domain\nname. In this case, the IP address would go to the `domain` field.', - example: 'www.elastic.co', - }, - { - name: 'extension', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The field contains the file extension from the original request\nurl.\n\nThe file extension is only set if it exists, as not every url has a file extension.\n\nThe leading period must not be included. For example, the value must be "png",\nnot ".png".', - example: 'png', - }, - { - name: 'fragment', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Portion of the url after the `#`, such as "top".\n\nThe `#` is not part of the fragment.', - }, - { - name: 'full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'If full URLs are important to your use case, they should be stored\nin `url.full`, whether this field is reconstructed or present in the event\nsource.', - example: 'https://www.elastic.co:443/search?q=elasticsearch#top', - }, - { - name: 'original', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'Unmodified original url as seen in the event source.\n\nNote that in network monitoring, the observed URL may be a full URL, whereas\nin access logs, the URL is often just represented as a path.\n\nThis field is meant to represent the URL as it was observed, complete or not.', - example: - 'https://www.elastic.co:443/search?q=elasticsearch#top or /search?q=elasticsearch', - }, - { - name: 'password', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Password of the request.', - }, - { - name: 'path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Path of the request, such as "/search".', - }, - { - name: 'port', - level: 'extended', - type: 'long', - format: 'string', - description: 'Port of the request, such as 443.', - example: 443, - }, - { - name: 'query', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The query field describes the query string of the request, such\nas "q=elasticsearch".\n\nThe `?` is excluded from the query string. If a URL contains no `?`, there\nis no query field. If there is a `?` but no query, the query field exists\nwith an empty string. The `exists` query can be used to differentiate between\nthe two cases.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered url domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'scheme', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Scheme of the request, such as "https".\n\nNote: The `:` is not part of the scheme.', - example: 'https', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'username', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Username of the request.', - }, - ], - }, - { - name: 'user', - title: 'User', - group: 2, - description: - 'The user fields describe information about the user that is relevant\nto the event.\n\nFields can have one entry or multiple entries. If a user has more than one id,\nprovide an array that includes all of them.', - type: 'group', - fields: [ - { - name: 'domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier of the user.', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'user_agent', - title: 'User agent', - group: 2, - description: - 'The user_agent fields normally come from a browser request.\n\nThey often show up in web service logs coming from the parsed user agent string.', - type: 'group', - fields: [ - { - name: 'device.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the device.', - example: 'iPhone', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the user agent.', - example: 'Safari', - }, - { - name: 'original', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: 'Unparsed user_agent string.', - example: - 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_1 like Mac OS X) AppleWebKit/605.1.15\n(KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1', - }, - { - name: 'os.family', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'os.full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'os.kernel', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'os.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'os.platform', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'os.version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Version of the user agent.', - example: 12, - }, - ], - }, - { - name: 'vlan', - title: 'VLAN', - group: 2, - description: - 'The VLAN fields are used to identify 802.1q tag(s) of a packet,\nas well as ingress and egress VLAN associations of an observer in relation to\na specific packet or connection.\n\nNetwork.vlan fields are used to record a single VLAN tag, or the outer tag in\nthe case of q-in-q encapsulations, for a packet or connection as observed, typically\nprovided by a network sensor (e.g. Zeek, Wireshark) passively reporting on traffic.\n\nNetwork.inner VLAN fields are used to report inner q-in-q 802.1q tags (multiple\n802.1q encapsulations) as observed, typically provided by a network sensor (e.g.\nZeek, Wireshark) passively reporting on traffic. Network.inner VLAN fields should\nonly be used in addition to network.vlan fields to indicate q-in-q tagging.\n\nObserver.ingress and observer.egress VLAN values are used to record observer\nspecific information when observer events contain discrete ingress and egress\nVLAN information, typically provided by firewalls, routers, or load balancers.', - type: 'group', - fields: [ - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - ], - }, - { - name: 'vulnerability', - title: 'Vulnerability', - group: 2, - description: - 'The vulnerability fields describe information about a vulnerability\nthat is relevant to an event.', - type: 'group', - fields: [ - { - name: 'category', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of system or architecture that the vulnerability affects.\nThese may be platform-specific (for example, Debian or SUSE) or general (for\nexample, Database or Firewall). For example (https://qualysguard.qualys.com/qwebhelp/fo_portal/knowledgebase/vulnerability_categories.htm[Qualys\nvulnerability categories])\n\nThis field must be an array.', - example: '["Firewall"]', - default_field: false, - }, - { - name: 'classification', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The classification of the vulnerability scoring system. For example\n(https://www.first.org/cvss/)', - example: 'CVSS', - default_field: false, - }, - { - name: 'description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: - 'The description of the vulnerability that provides additional context\nof the vulnerability. For example (https://cve.mitre.org/about/faqs.html#cve_entry_descriptions_created[Common\nVulnerabilities and Exposure CVE description])', - example: 'In macOS before 2.12.6, there is a vulnerability in the RPC...', - default_field: false, - }, - { - name: 'enumeration', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of identifier used for this vulnerability. For example\n(https://cve.mitre.org/about/)', - example: 'CVE', - default_field: false, - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The identification (ID) is the number portion of a vulnerability\nentry. It includes a unique identification number for the vulnerability. For\nexample (https://cve.mitre.org/about/faqs.html#what_is_cve_id)[Common Vulnerabilities\nand Exposure CVE ID]', - example: 'CVE-2019-00001', - default_field: false, - }, - { - name: 'reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A resource that provides additional information, context, and mitigations\nfor the identified vulnerability.', - example: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-6111', - default_field: false, - }, - { - name: 'report_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The report or scan identification number.', - example: 20191018.0001, - default_field: false, - }, - { - name: 'scanner.vendor', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The name of the vulnerability scanner vendor.', - example: 'Tenable', - default_field: false, - }, - { - name: 'score.base', - level: 'extended', - type: 'float', - description: - 'Scores can range from 0.0 to 10.0, with 10.0 being the most severe.\n\nBase scores cover an assessment for exploitability metrics (attack vector,\ncomplexity, privileges, and user interaction), impact metrics (confidentiality,\nintegrity, and availability), and scope. For example (https://www.first.org/cvss/specification-document)', - example: 5.5, - default_field: false, - }, - { - name: 'score.environmental', - level: 'extended', - type: 'float', - description: - 'Scores can range from 0.0 to 10.0, with 10.0 being the most severe.\n\nEnvironmental scores cover an assessment for any modified Base metrics, confidentiality,\nintegrity, and availability requirements. For example (https://www.first.org/cvss/specification-document)', - example: 5.5, - default_field: false, - }, - { - name: 'score.temporal', - level: 'extended', - type: 'float', - description: - 'Scores can range from 0.0 to 10.0, with 10.0 being the most severe.\n\nTemporal scores cover an assessment for code maturity, remediation level,\nand confidence. For example (https://www.first.org/cvss/specification-document)', - default_field: false, - }, - { - name: 'score.version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The National Vulnerability Database (NVD) provides qualitative\nseverity rankings of "Low", "Medium", and "High" for CVSS v2.0 base score\nranges in addition to the severity ratings for CVSS v3.0 as they are defined\nin the CVSS v3.0 specification.\n\nCVSS is owned and managed by FIRST.Org, Inc. (FIRST), a US-based non-profit\norganization, whose mission is to help computer security incident response\nteams across the world. For example (https://nvd.nist.gov/vuln-metrics/cvss)', - example: 2, - default_field: false, - }, - { - name: 'severity', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The severity of the vulnerability can help with metrics and internal\nprioritization regarding remediation. For example (https://nvd.nist.gov/vuln-metrics/cvss)', - example: 'Critical', - default_field: false, - }, - ], - }, - ], - }, -]; diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/filebeat.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/filebeat.ts deleted file mode 100644 index 3b8c92ebba269..0000000000000 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/filebeat.ts +++ /dev/null @@ -1,21243 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -/** - * An instance of the unmodified schema exported from filebeat-8.0.0-SNAPSHOT-darwin-x86_64.tar.gz - * - */ - -import { Schema } from '../type'; - -export const filebeatSchema: Schema = [ - { - key: 'ecs', - title: 'ECS', - description: 'ECS Fields.', - fields: [ - { - name: '@timestamp', - level: 'core', - required: true, - type: 'date', - description: - 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', - example: '2016-05-23T08:05:34.853Z', - }, - { - name: 'labels', - level: 'core', - type: 'object', - object_type: 'keyword', - description: - 'Custom key/value pairs.\n\nCan be used to add meta information to events. Should not contain nested objects.\nAll values are stored as keyword.\n\nExample: `docker` and `k8s` labels.', - example: '{"application": "foo-bar", "env": "production"}', - }, - { - name: 'message', - level: 'core', - type: 'text', - description: - 'For log events the message field contains the log message, optimized\nfor viewing in a log viewer.\n\nFor structured logs without an original message field, other fields can be concatenated\nto form a human-readable summary of the event.\n\nIf multiple messages exist, they can be combined into one message.', - example: 'Hello World', - }, - { - name: 'tags', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'List of keywords used to tag each event.', - example: '["production", "env2"]', - }, - { - name: 'agent', - title: 'Agent', - group: 2, - description: - 'The agent fields contain the data about the software entity, if\nany, that collects, detects, or observes events on a host, or takes measurements\non a host.\n\nExamples include Beats. Agents may also run on observers. ECS agent.* fields\nshall be populated with details of the agent running on the host or observer\nwhere the event happened or the measurement was taken.', - footnote: - 'Examples: In the case of Beats for logs, the agent.name is filebeat.\nFor APM, it is the agent running in the app/service. The agent information does\nnot change if data is sent through queuing systems like Kafka, Redis, or processing\nsystems such as Logstash or APM Server.', - type: 'group', - fields: [ - { - name: 'ephemeral_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Ephemeral identifier of this agent (if one exists).\n\nThis id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier of this agent (if one exists).\n\nExample: For Beats this would be beat.id.', - example: '8a4f500d', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', - example: 'foo', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', - example: 'filebeat', - }, - { - name: 'version', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Version of the agent.', - example: '6.0.0-rc2', - }, - ], - }, - { - name: 'as', - title: 'Autonomous System', - group: 2, - description: - 'An autonomous system (AS) is a collection of connected Internet Protocol\n(IP) routing prefixes under the control of one or more network operators on\nbehalf of a single administrative entity or domain that presents a common, clearly\ndefined routing policy to the internet.', - type: 'group', - fields: [ - { - name: 'number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - ], - }, - { - name: 'client', - title: 'Client', - group: 2, - description: - 'A client is defined as the initiator of a network connection for\nevents regarding sessions, connections, or bidirectional flow records.\n\nFor TCP events, the client is the initiator of the TCP connection that sends\nthe SYN packet(s). For other protocols, the client is generally the initiator\nor requestor in the network transaction. Some systems use the term "originator"\nto refer the client in TCP connections. The client fields describe details about\nthe system acting as the client in the network event. Client fields are usually\npopulated in conjunction with server fields. Client fields are generally not\npopulated for packet-level events.\n\nClient / server representations can add semantic context to an exchange, which\nis helpful to visualize the data in certain situations. If your context falls\nin that category, you should still ensure that source and destination are filled\nappropriately.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Some event client addresses are defined ambiguously. The event\nwill sometimes list an IP, a domain or a unix socket. You should always store\nthe raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'as.number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'as.organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the client to the server.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Client domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: - 'IP address of the client.\n\nCan be one or multiple IPv4 or IPv6 addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC address of the client.', - }, - { - name: 'nat.ip', - level: 'extended', - type: 'ip', - description: - 'Translated IP of source based NAT sessions (e.g. internal client\nto internet).\n\nTypically connections traversing load balancers, firewalls, or routers.', - }, - { - name: 'nat.port', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Translated port of source based NAT sessions (e.g. internal client\nto internet).\n\nTypically connections traversing load balancers, firewalls, or routers.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the client to the server.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the client.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered client domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'cloud', - title: 'Cloud', - group: 2, - description: 'Fields related to the cloud or infrastructure the events are coming\nfrom.', - footnote: - 'Examples: If Metricbeat is running on an EC2 host and fetches data\nfrom its host, the cloud info contains the data about this machine. If Metricbeat\nruns on a remote machine outside the cloud and fetches data from a service running\nin the cloud, the field contains cloud data from the machine the service is\nrunning on.', - type: 'group', - fields: [ - { - name: 'account.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The cloud account or organization id used to identify different\nentities in a multi-tenant environment.\n\nExamples: AWS account id, Google Cloud ORG Id, or other unique identifier.', - example: 666777888999, - }, - { - name: 'availability_zone', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Availability zone in which this host is running.', - example: 'us-east-1c', - }, - { - name: 'instance.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Instance ID of the host machine.', - example: 'i-1234567890abcdef0', - }, - { - name: 'instance.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Instance name of the host machine.', - }, - { - name: 'machine.type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Machine type of the host machine.', - example: 't2.medium', - }, - { - name: 'provider', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the cloud provider. Example values are aws, azure, gcp,\nor digitalocean.', - example: 'aws', - }, - { - name: 'region', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Region in which this host is running.', - example: 'us-east-1', - }, - ], - }, - { - name: 'code_signature', - title: 'Code Signature', - group: 2, - description: 'These fields contain information about binary code signatures.', - type: 'group', - fields: [ - { - name: 'exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - ], - }, - { - name: 'container', - title: 'Container', - group: 2, - description: - 'Container fields are used for meta information about the specific\ncontainer that is the source of information.\n\nThese fields help correlate data based containers from any runtime.', - type: 'group', - fields: [ - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique container id.', - }, - { - name: 'image.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the image the container was built on.', - }, - { - name: 'image.tag', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Container image tags.', - }, - { - name: 'labels', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: 'Image labels.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Container name.', - }, - { - name: 'runtime', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Runtime managing this container.', - example: 'docker', - }, - ], - }, - { - name: 'destination', - title: 'Destination', - group: 2, - description: - 'Destination fields describe details about the destination of a packet/event.\n\nDestination fields are usually populated in conjunction with source fields.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Some event destination addresses are defined ambiguously. The\nevent will sometimes list an IP, a domain or a unix socket. You should always\nstore the raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'as.number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'as.organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the destination to the source.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Destination domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: - 'IP address of the destination.\n\nCan be one or multiple IPv4 or IPv6 addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC address of the destination.', - }, - { - name: 'nat.ip', - level: 'extended', - type: 'ip', - description: - 'Translated ip of destination based NAT sessions (e.g. internet\nto private DMZ)\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'nat.port', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Port the source session is translated to by NAT Device.\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the destination to the source.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the destination.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered destination domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'dll', - title: 'DLL', - group: 2, - description: - 'These fields contain information about code libraries dynamically\nloaded into processes.\n\n\nMany operating systems refer to "shared code libraries" with different names,\nbut this field set refers to all of the following:\n\n* Dynamic-link library (`.dll`) commonly used on Windows\n\n* Shared Object (`.so`) commonly used on Unix-like operating systems\n\n* Dynamic library (`.dylib`) commonly used on macOS', - type: 'group', - fields: [ - { - name: 'code_signature.exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'code_signature.subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'code_signature.trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - { - name: 'hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - default_field: false, - }, - { - name: 'hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - default_field: false, - }, - { - name: 'hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - default_field: false, - }, - { - name: 'hash.sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - default_field: false, - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the library.\n\nThis generally maps to the name of the file on disk.', - example: 'kernel32.dll', - default_field: false, - }, - { - name: 'path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Full file path of the library.', - example: 'C:\\Windows\\System32\\kernel32.dll', - default_field: false, - }, - { - name: 'pe.company', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal company name of the file, provided at compile-time.', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'pe.description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal description of the file, provided at compile-time.', - example: 'Paint', - default_field: false, - }, - { - name: 'pe.file_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal version of the file, provided at compile-time.', - example: '6.3.9600.17415', - default_field: false, - }, - { - name: 'pe.original_file_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal name of the file, provided at compile-time.', - example: 'MSPAINT.EXE', - default_field: false, - }, - { - name: 'pe.product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal product name of the file, provided at compile-time.', - example: 'Microsoft® Windows® Operating System', - default_field: false, - }, - ], - }, - { - name: 'dns', - title: 'DNS', - group: 2, - description: - 'Fields describing DNS queries and answers.\n\nDNS events should either represent a single DNS query prior to getting answers\n(`dns.type:query`) or they should represent a full exchange and contain the\nquery details as well as all of the answers that were provided for this query\n(`dns.type:answer`).', - type: 'group', - fields: [ - { - name: 'answers', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'An array containing an object for each answer section returned\nby the server.\n\nThe main keys that should be present in these objects are defined by ECS.\nRecords that have more information may contain more keys than what ECS defines.\n\nNot all DNS data sources give all details about DNS answers. At minimum, answer\nobjects must contain the `data` key. If more information is available, map\nas much of it to ECS as possible, and add any additional fields to the answer\nobjects as custom fields.', - }, - { - name: 'answers.class', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The class of DNS data contained in this resource record.', - example: 'IN', - }, - { - name: 'answers.data', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The data describing the resource.\n\nThe meaning of this data depends on the type and class of the resource record.', - example: '10.10.10.10', - }, - { - name: 'answers.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The domain name to which this resource record pertains.\n\nIf a chain of CNAME is being resolved, each answer `name` should be the\none that corresponds with the answer `data`. It should not simply be the\noriginal `question.name` repeated.', - example: 'www.google.com', - }, - { - name: 'answers.ttl', - level: 'extended', - type: 'long', - description: - 'The time interval in seconds that this resource record may be cached\nbefore it should be discarded. Zero values mean that the data should not be\ncached.', - example: 180, - }, - { - name: 'answers.type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The type of data contained in this resource record.', - example: 'CNAME', - }, - { - name: 'header_flags', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of 2 letter DNS header flags.\n\nExpected values are: AA, TC, RD, RA, AD, CD, DO.', - example: ['RD', 'RA'], - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The DNS packet identifier assigned by the program that generated\nthe query. The identifier is copied to the response.', - example: 62111, - }, - { - name: 'op_code', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The DNS operation code that specifies the kind of query in the\nmessage. This value is set by the originator of a query and copied into the\nresponse.', - example: 'QUERY', - }, - { - name: 'question.class', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The class of records being queried.', - example: 'IN', - }, - { - name: 'question.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The name being queried.\n\nIf the name field contains non-printable characters (below 32 or above 126),\nthose characters should be represented as escaped base 10 integers (\\DDD).\nBack slashes and quotes should be escaped. Tabs, carriage returns, and line\nfeeds should be converted to \\t, \\r, and \\n respectively.', - example: 'www.google.com', - }, - { - name: 'question.registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'question.subdomain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The subdomain is all of the labels under the registered_domain.\n\nIf the domain has multiple levels of subdomain, such as "sub2.sub1.example.com",\nthe subdomain field should contain "sub2.sub1", with no trailing period.', - example: 'www', - }, - { - name: 'question.top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'question.type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The type of record being queried.', - example: 'AAAA', - }, - { - name: 'resolved_ip', - level: 'extended', - type: 'ip', - description: - 'Array containing all IPs seen in `answers.data`.\n\nThe `answers` array can be difficult to use, because of the variety of data\nformats it can contain. Extracting all IP addresses seen in there to `dns.resolved_ip`\nmakes it possible to index them as IP addresses, and makes them easier to\nvisualize and query for.', - example: ['10.10.10.10', '10.10.10.11'], - }, - { - name: 'response_code', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The DNS response code.', - example: 'NOERROR', - }, - { - name: 'type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of DNS event captured, query or answer.\n\nIf your source of DNS events only gives you DNS queries, you should only create\ndns events of type `dns.type:query`.\n\nIf your source of DNS events gives you answers as well, you should create\none event per query (optionally as soon as the query is seen). And a second\nevent containing all query details as well as an array of answers.', - example: 'answer', - }, - ], - }, - { - name: 'ecs', - title: 'ECS', - group: 2, - description: 'Meta-information specific to ECS.', - type: 'group', - fields: [ - { - name: 'version', - level: 'core', - required: true, - type: 'keyword', - ignore_above: 1024, - description: - 'ECS version this event conforms to. `ecs.version` is a required\nfield and must exist in all events.\n\nWhen querying across multiple indices -- which may conform to slightly different\nECS versions -- this field lets integrations adjust to the schema version\nof the events.', - example: '1.0.0', - }, - ], - }, - { - name: 'error', - title: 'Error', - group: 2, - description: - 'These fields can represent errors of any kind.\n\nUse them for errors that happen while fetching events or in cases where the\nevent itself contains an error.', - type: 'group', - fields: [ - { - name: 'code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Error code describing the error.', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the error.', - }, - { - name: 'message', - level: 'core', - type: 'text', - description: 'Error message.', - }, - { - name: 'stack_trace', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'The stack trace of this error in plain text.', - }, - { - name: 'type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The type of the error, for example the class name of the exception.', - example: 'java.lang.NullPointerException', - }, - ], - }, - { - name: 'event', - title: 'Event', - group: 2, - description: - 'The event fields are used for context information about the log\nor metric event itself.\n\nA log is defined as an event containing details of something that happened.\nLog events must include the time at which the thing happened. Examples of log\nevents include a process starting on a host, a network packet being sent from\na source to a destination, or a network connection between a client and a server\nbeing initiated or closed. A metric is defined as an event containing one or\nmore numerical measurements and the time at which the measurement was taken.\nExamples of metric events include memory pressure measured on a host and device\ntemperature. See the `event.kind` definition in this section for additional\ndetails about metric and state events.', - type: 'group', - fields: [ - { - name: 'action', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.', - example: 'user-password-change', - }, - { - name: 'category', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is one of four ECS Categorization Fields, and indicates the\nsecond level in the ECS category hierarchy.\n\n`event.category` represents the "big buckets" of ECS categories. For example,\nfiltering on `event.category:process` yields all events relating to process\nactivity. This field is closely related to `event.type`, which is used as\na subcategory.\n\nThis field is an array. This will allow proper categorization of some events\nthat fall in multiple categories.', - example: 'authentication', - }, - { - name: 'code', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Identification code for this event, if one exists.\n\nSome event sources use event codes to identify messages unambiguously, regardless\nof message language or wording adjustments over time. An example of this is\nthe Windows Event ID.', - example: 4648, - }, - { - name: 'created', - level: 'core', - type: 'date', - description: - 'event.created contains the date/time when the event was first\nread by an agent, or by your pipeline.\n\nThis field is distinct from @timestamp in that @timestamp typically contain\nthe time extracted from the original event.\n\nIn most situations, these two timestamps will be slightly different. The difference\ncan be used to calculate the delay between your source generating an event,\nand the time when your agent first processed it. This can be used to monitor\nyour agent or pipeline ability to keep up with your event source.\n\nIn case the two timestamps are identical, @timestamp should be used.', - example: '2016-05-23T08:05:34.857Z', - }, - { - name: 'dataset', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the dataset.\n\nIf an event source publishes more than one type of log or events (e.g. access\nlog, error log), the dataset is used to specify which one the event comes\nfrom.\n\nIt is recommended but not required to start the dataset name with the module\nname, followed by a dot, then the dataset name.', - example: 'apache.access', - }, - { - name: 'duration', - level: 'core', - type: 'long', - format: 'duration', - input_format: 'nanoseconds', - output_format: 'asMilliseconds', - output_precision: 1, - description: - 'Duration of the event in nanoseconds.\n\nIf event.start and event.end are known this value should be the difference\nbetween the end and start time.', - }, - { - name: 'end', - level: 'extended', - type: 'date', - description: - 'event.end contains the date when the event ended or when the activity\nwas last observed.', - }, - { - name: 'hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Hash (perhaps logstash fingerprint) of raw field to be able to\ndemonstrate log integrity.', - example: '123456789012345678901234567890ABCD', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique ID to describe the event.', - example: '8a4f500d', - }, - { - name: 'ingested', - level: 'core', - type: 'date', - description: - 'Timestamp when an event arrived in the central data store.\n\nThis is different from `@timestamp`, which is when the event originally occurred. It is\nalso different from `event.created`, which is meant to capture the first time\nan agent saw the event.\n\nIn normal conditions, assuming no tampering, the timestamps should chronologically\nlook like this: `@timestamp` < `event.created` < `event.ingested`.', - example: '2016-05-23T08:05:35.101Z', - default_field: false, - }, - { - name: 'kind', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is one of four ECS Categorization Fields, and indicates the\nhighest level in the ECS category hierarchy.\n\n`event.kind` gives high-level information about what type of information the\nevent contains, without being specific to the contents of the event. For example,\nvalues of this field distinguish alert events from metric events.\n\nThe value of this field can be used to inform how these kinds of events should\nbe handled. They may warrant different retention, different access control,\nit may also help understand whether the data coming in at a regular interval\nor not.', - example: 'alert', - }, - { - name: 'module', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the module this data is coming from.\n\nIf your monitoring agent supports the concept of modules or plugins to process\nevents of a given source (e.g. Apache logs), `event.module` should contain\nthe name of this module.', - example: 'apache', - }, - { - name: 'original', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Raw text message of entire event. Used to demonstrate log integrity.\n\nThis field is not indexed and doc_values are disabled. It cannot be searched,\nbut it can be retrieved from `_source`.', - example: - 'Sep 19 08:26:10 host CEF:0|Security| threatmanager|1.0|100|\nworm successfully stopped|10|src=10.0.0.1 dst=2.1.2.2spt=1232', - }, - { - name: 'outcome', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is one of four ECS Categorization Fields, and indicates the\nlowest level in the ECS category hierarchy.\n\n`event.outcome` simply denotes whether the event represents a success or a\nfailure from the perspective of the entity that produced the event.\n\nNote that when a single transaction is described in multiple events, each\nevent may populate different values of `event.outcome`, according to their\nperspective.\n\nAlso note that in the case of a compound event (a single event that contains\nmultiple logical events), this field should be populated with the value that\nbest captures the overall success or failure from the perspective of the event\nproducer.\n\nFurther note that not all events will have an associated outcome. For example,\nthis field is generally not populated for metric events, events with `event.type:info`,\nor any events for which an outcome does not make logical sense.', - example: 'success', - }, - { - name: 'provider', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Source of the event.\n\nEvent transports such as Syslog or the Windows Event Log typically mention\nthe source of an event. It can be the name of the software that generated\nthe event (e.g. Sysmon, httpd), or of a subsystem of the operating system\n(kernel, Microsoft-Windows-Security-Auditing).', - example: 'kernel', - }, - { - name: 'reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Reference URL linking to additional information about this event.\n\nThis URL links to a static definition of the this event. Alert events, indicated\nby `event.kind:alert`, are a common use case for this field.', - example: 'https://system.vendor.com/event/#0001234', - default_field: false, - }, - { - name: 'risk_score', - level: 'core', - type: 'float', - description: - "Risk score or priority of the event (e.g. security solutions).\nUse your system's original value here.", - }, - { - name: 'risk_score_norm', - level: 'extended', - type: 'float', - description: - 'Normalized risk score or priority of the event, on a scale of\n0 to 100.\n\nThis is mainly useful if you use more than one system that assigns risk scores,\nand you want to see a normalized value across all systems.', - }, - { - name: 'sequence', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Sequence number of the event.\n\nThe sequence number is a value published by some event sources, to make the\nexact ordering of events unambiguous, regardless of the timestamp precision.', - }, - { - name: 'severity', - level: 'core', - type: 'long', - format: 'string', - description: - 'The numeric severity of the event according to your event source.\n\nWhat the different severity values mean can be different between sources and\nuse cases. It is up to the implementer to make sure severities are consistent\nacross events from the same source.\n\nThe Syslog severity belongs in `log.syslog.severity.code`. `event.severity`\nis meant to represent the severity according to the event source (e.g. firewall,\nIDS). If the event source does not publish its own severity, you may optionally\ncopy the `log.syslog.severity.code` to `event.severity`.', - example: 7, - }, - { - name: 'start', - level: 'extended', - type: 'date', - description: - 'event.start contains the date when the event started or when the\nactivity was first observed.', - }, - { - name: 'timezone', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'This field should be populated when the event timestamp does\nnot include timezone information already (e.g. default Syslog timestamps).\nIt is optional otherwise.\n\nAcceptable timezone formats are: a canonical ID (e.g. "Europe/Amsterdam"),\nabbreviated (e.g. "EST") or an HH:mm differential (e.g. "-05:00").', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is one of four ECS Categorization Fields, and indicates the\nthird level in the ECS category hierarchy.\n\n`event.type` represents a categorization "sub-bucket" that, when used along\nwith the `event.category` field values, enables filtering events down to a\nlevel appropriate for single visualization.\n\nThis field is an array. This will allow proper categorization of some events\nthat fall in multiple event types.', - }, - { - name: 'url', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'URL linking to an external system to continue investigation of\nthis event.\n\nThis URL links to another system where in-depth investigation of the specific\noccurence of this event can take place. Alert events, indicated by `event.kind:alert`,\nare a common use case for this field.', - example: 'https://mysystem.mydomain.com/alert/5271dedb-f5b0-4218-87f0-4ac4870a38fe', - default_field: false, - }, - ], - }, - { - name: 'file', - title: 'File', - group: 2, - description: - 'A file is defined as a set of information that has been created\non, or has existed on a filesystem.\n\nFile objects can be associated with host events, network events, and/or file\nevents (e.g., those produced by File Integrity Monitoring [FIM] products or\nservices). File fields provide details about the affected file associated with\nthe event or metric.', - type: 'group', - fields: [ - { - name: 'accessed', - level: 'extended', - type: 'date', - description: - 'Last time the file was accessed.\n\nNote that not all filesystems keep track of access time.', - }, - { - name: 'attributes', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of file attributes.\n\nAttributes names will vary by platform. Here is a non-exhaustive list of values\nthat are expected in this field: archive, compressed, directory, encrypted,\nexecute, hidden, read, readonly, system, write.', - example: '["readonly", "system"]', - default_field: false, - }, - { - name: 'code_signature.exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'code_signature.subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'code_signature.trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - { - name: 'created', - level: 'extended', - type: 'date', - description: - 'File creation time.\n\nNote that not all filesystems store the creation time.', - }, - { - name: 'ctime', - level: 'extended', - type: 'date', - description: - 'Last time the file attributes or metadata changed.\n\nNote that changes to the file content will update `mtime`. This implies `ctime`\nwill be adjusted at the same time, since `mtime` is an attribute of the file.', - }, - { - name: 'device', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Device that is the source of the file.', - example: 'sda', - }, - { - name: 'directory', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Directory where the file is located. It should include the drive\nletter, when appropriate.', - example: '/home/alice', - }, - { - name: 'drive_letter', - level: 'extended', - type: 'keyword', - ignore_above: 1, - description: - 'Drive letter where the file is located. This field is only relevant\non Windows.\n\nThe value should be uppercase, and not include the colon.', - example: 'C', - default_field: false, - }, - { - name: 'extension', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'File extension.', - example: 'png', - }, - { - name: 'gid', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Primary group ID (GID) of the file.', - example: '1001', - }, - { - name: 'group', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Primary group name of the file.', - example: 'alice', - }, - { - name: 'hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - }, - { - name: 'hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - }, - { - name: 'hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - }, - { - name: 'hash.sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - }, - { - name: 'inode', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Inode representing the file in the filesystem.', - example: '256383', - }, - { - name: 'mime_type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'MIME type should identify the format of the file or stream of bytes\nusing https://www.iana.org/assignments/media-types/media-types.xhtml[IANA\nofficial types], where possible. When more than one type is applicable, the\nmost specific type should be used.', - default_field: false, - }, - { - name: 'mode', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Mode of the file in octal representation.', - example: '0640', - }, - { - name: 'mtime', - level: 'extended', - type: 'date', - description: 'Last time the file content was modified.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the file including the extension, without the directory.', - example: 'example.png', - }, - { - name: 'owner', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: "File owner's username.", - example: 'alice', - }, - { - name: 'path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'Full path to the file, including the file name. It should include\nthe drive letter, when appropriate.', - example: '/home/alice/example.png', - }, - { - name: 'pe.company', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal company name of the file, provided at compile-time.', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'pe.description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal description of the file, provided at compile-time.', - example: 'Paint', - default_field: false, - }, - { - name: 'pe.file_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal version of the file, provided at compile-time.', - example: '6.3.9600.17415', - default_field: false, - }, - { - name: 'pe.original_file_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal name of the file, provided at compile-time.', - example: 'MSPAINT.EXE', - default_field: false, - }, - { - name: 'pe.product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal product name of the file, provided at compile-time.', - example: 'Microsoft® Windows® Operating System', - default_field: false, - }, - { - name: 'size', - level: 'extended', - type: 'long', - description: 'File size in bytes.\n\nOnly relevant when `file.type` is "file".', - example: 16384, - }, - { - name: 'target_path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Target path for symlinks.', - }, - { - name: 'type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'File type (file, dir, or symlink).', - example: 'file', - }, - { - name: 'uid', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The user ID (UID) or security identifier (SID) of the file owner.', - example: '1001', - }, - ], - }, - { - name: 'geo', - title: 'Geo', - group: 2, - description: - 'Geo fields can carry data about a specific location related to an\nevent.\n\nThis geolocation information can be derived from techniques such as Geo IP,\nor be user-supplied.', - type: 'group', - fields: [ - { - name: 'city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - ], - }, - { - name: 'group', - title: 'Group', - group: 2, - description: - 'The group fields are meant to represent groups that are relevant\nto the event.', - type: 'group', - fields: [ - { - name: 'domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - ], - }, - { - name: 'hash', - title: 'Hash', - group: 2, - description: - 'The hash fields represent different hash algorithms and their values.\n\nField names for common hashes (e.g. MD5, SHA1) are predefined. Add fields for\nother hashes by lowercasing the hash algorithm name and using underscore separators\nas appropriate (snake case, e.g. sha3_512).', - type: 'group', - fields: [ - { - name: 'md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - }, - { - name: 'sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - }, - { - name: 'sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - }, - { - name: 'sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - }, - ], - }, - { - name: 'host', - title: 'Host', - group: 2, - description: - 'A host is defined as a general computing instance.\n\nECS host.* fields should be populated with details about the host on which the\nevent happened, or from which the measurement was taken. Host types include\nhardware, virtual machines, Docker containers, and Kubernetes nodes.', - type: 'group', - fields: [ - { - name: 'architecture', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system architecture.', - example: 'x86_64', - }, - { - name: 'domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the domain of which the host is a member.\n\nFor example, on Windows this could be the host Active Directory domain\nor NetBIOS domain name. For Linux this could be the domain of the host\nLDAP provider.', - example: 'CONTOSO', - default_field: false, - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'hostname', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Hostname of the host.\n\nIt normally contains what the `hostname` command returns on the host machine.', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique host id.\n\nAs hostname is not always unique, use values that are meaningful in your environment.\n\nExample: The current usage of `beat.name`.', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: 'Host ip addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Host mac addresses.', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.', - }, - { - name: 'os.family', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'os.full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'os.kernel', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'os.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'os.platform', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'os.version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Type of host.\n\nFor Cloud providers this can be the machine type like `t2.medium`. If vm,\nthis could be the container, for example, or other information meaningful\nin your environment.', - }, - { - name: 'uptime', - level: 'extended', - type: 'long', - description: 'Seconds the host has been up.', - example: 1325, - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'http', - title: 'HTTP', - group: 2, - description: - 'Fields related to HTTP activity. Use the `url` field set to store\nthe url of the request.', - type: 'group', - fields: [ - { - name: 'request.body.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Size in bytes of the request body.', - example: 887, - }, - { - name: 'request.body.content', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'The full HTTP request body.', - example: 'Hello world', - }, - { - name: 'request.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Total size in bytes of the request (body and headers).', - example: 1437, - }, - { - name: 'request.method', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'HTTP request method.\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'get, post, put', - }, - { - name: 'request.referrer', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Referrer for this HTTP request.', - example: 'https://blog.example.com/', - }, - { - name: 'response.body.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Size in bytes of the response body.', - example: 887, - }, - { - name: 'response.body.content', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'The full HTTP response body.', - example: 'Hello world', - }, - { - name: 'response.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Total size in bytes of the response (body and headers).', - example: 1437, - }, - { - name: 'response.status_code', - level: 'extended', - type: 'long', - format: 'string', - description: 'HTTP response status code.', - example: 404, - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'HTTP version.', - example: 1.1, - }, - ], - }, - { - name: 'interface', - title: 'Interface', - group: 2, - description: - 'The interface fields are used to record ingress and egress interface\ninformation when reported by an observer (e.g. firewall, router, load balancer)\nin the context of the observer handling a network connection. In the case of\na single observer interface (e.g. network sensor on a span port) only the observer.ingress\ninformation should be populated.', - type: 'group', - fields: [ - { - name: 'alias', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Interface alias as reported by the system, typically used in firewall\nimplementations for e.g. inside, outside, or dmz logical interface naming.', - example: 'outside', - default_field: false, - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface ID as reported by an observer (typically SNMP interface\nID).', - example: 10, - default_field: false, - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface name as reported by the system.', - example: 'eth0', - default_field: false, - }, - ], - }, - { - name: 'log', - title: 'Log', - group: 2, - description: - 'Details about the event logging mechanism or logging transport.\n\nThe log.* fields are typically populated with details about the logging mechanism\nused to create and/or transport the event. For example, syslog details belong\nunder `log.syslog.*`.\n\nThe details specific to your event source are typically not logged under `log.*`,\nbut rather in `event.*` or in other ECS fields.', - type: 'group', - fields: [ - { - name: 'level', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Original log level of the log event.\n\nIf the source of the event provides a log level or textual severity, this\nis the one that goes in `log.level`. If your source does not specify one,\nyou may put your event transport severity here (e.g. Syslog severity).\n\nSome examples are `warn`, `err`, `i`, `informational`.', - example: 'error', - }, - { - name: 'logger', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'The name of the logger inside an application. This is usually the\nname of the class which initialized the logger, or can be a custom name.', - example: 'org.elasticsearch.bootstrap.Bootstrap', - }, - { - name: 'origin.file.line', - level: 'extended', - type: 'integer', - description: - 'The line number of the file containing the source code which originated\nthe log event.', - example: 42, - }, - { - name: 'origin.file.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The name of the file containing the source code which originated\nthe log event. Note that this is not the name of the log file.', - example: 'Bootstrap.java', - }, - { - name: 'origin.function', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The name of the function or method which originated the log event.', - example: 'init', - }, - { - name: 'original', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is the original log message and contains the full log message\nbefore splitting it up in multiple parts.\n\nIn contrast to the `message` field which can contain an extracted part of\nthe log message, this field contains the original, full log message. It can\nhave already some modifications applied like encoding or new lines removed\nto clean up the log message.\n\nThis field is not indexed and doc_values are disabled so it cannot be queried\nbut the value can be retrieved from `_source`.', - example: 'Sep 19 08:26:10 localhost My log', - }, - { - name: 'syslog', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'The Syslog metadata of the event, if the event was transmitted\nvia Syslog. Please see RFCs 5424 or 3164.', - }, - { - name: 'syslog.facility.code', - level: 'extended', - type: 'long', - format: 'string', - description: - 'The Syslog numeric facility of the log event, if available.\n\nAccording to RFCs 5424 and 3164, this value should be an integer between 0\nand 23.', - example: 23, - }, - { - name: 'syslog.facility.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The Syslog text-based facility of the log event, if available.', - example: 'local7', - }, - { - name: 'syslog.priority', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Syslog numeric priority of the event, if available.\n\nAccording to RFCs 5424 and 3164, the priority is 8 * facility + severity.\nThis number is therefore expected to contain a value between 0 and 191.', - example: 135, - }, - { - name: 'syslog.severity.code', - level: 'extended', - type: 'long', - description: - 'The Syslog numeric severity of the log event, if available.\n\nIf the event source publishing via Syslog provides a different numeric severity\nvalue (e.g. firewall, IDS), your source numeric severity should go to `event.severity`.\nIf the event source does not specify a distinct severity, you can optionally\ncopy the Syslog severity to `event.severity`.', - example: 3, - }, - { - name: 'syslog.severity.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The Syslog numeric severity of the log event, if available.\n\nIf the event source publishing via Syslog provides a different severity value\n(e.g. firewall, IDS), your source text severity should go to `log.level`.\nIf the event source does not specify a distinct severity, you can optionally\ncopy the Syslog severity to `log.level`.', - example: 'Error', - }, - ], - }, - { - name: 'network', - title: 'Network', - group: 2, - description: - 'The network is defined as the communication path over which a host\nor network event happens.\n\nThe network.* fields should be populated with details about the network activity\nassociated with an event.', - type: 'group', - fields: [ - { - name: 'application', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A name given to an application level protocol. This can be arbitrarily\nassigned for things like microservices, but also apply to things like skype,\nicq, facebook, twitter. This would be used in situations where the vendor\nor service can be decoded such as from the source/dest IP owners, ports, or\nwire format.\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'aim', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: - 'Total bytes transferred in both directions.\n\nIf `source.bytes` and `destination.bytes` are known, `network.bytes` is their\nsum.', - example: 368, - }, - { - name: 'community_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A hash of source and destination IPs and ports, as well as the\nprotocol used in a communication. This is a tool-agnostic standard to identify\nflows.\n\nLearn more at https://github.com/corelight/community-id-spec.', - example: '1:hO+sN4H+MG5MY/8hIrXPqc4ZQz0=', - }, - { - name: 'direction', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - "Direction of the network traffic.\nRecommended values are:\n * inbound\n * outbound\n * internal\n * external\n * unknown\n\nWhen mapping events from a host-based monitoring context, populate this field from the host's point of view.\nWhen mapping events from a network or perimeter-based monitoring context, populate this field from the point of view of your network perimeter.", - example: 'inbound', - }, - { - name: 'forwarded_ip', - level: 'core', - type: 'ip', - description: 'Host IP address when the source IP address is the proxy.', - example: '192.1.1.2', - }, - { - name: 'iana_number', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'IANA Protocol Number (https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml).\nStandardized list of protocols. This aligns well with NetFlow and sFlow related\nlogs which use the IANA Protocol Number.', - example: 6, - }, - { - name: 'inner', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'Network.inner fields are added in addition to network.vlan fields\nto describe the innermost VLAN when q-in-q VLAN tagging is present. Allowed\nfields include vlan.id and vlan.name. Inner vlan fields are typically used\nwhen sending traffic with multiple 802.1q encapsulations to a network sensor\n(e.g. Zeek, Wireshark.)', - default_field: false, - }, - { - name: 'inner.vlan.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'inner.vlan.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name given by operators to sections of their network.', - example: 'Guest Wifi', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: - 'Total packets transferred in both directions.\n\nIf `source.packets` and `destination.packets` are known, `network.packets`\nis their sum.', - example: 24, - }, - { - name: 'protocol', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'L7 Network protocol name. ex. http, lumberjack, transport protocol.\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'http', - }, - { - name: 'transport', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Same as network.iana_number, but instead using the Keyword name\nof the transport layer (udp, tcp, ipv6-icmp, etc.)\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'tcp', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'In the OSI Model this would be the Network Layer. ipv4, ipv6,\nipsec, pim, etc\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'ipv4', - }, - { - name: 'vlan.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'vlan.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - ], - }, - { - name: 'observer', - title: 'Observer', - group: 2, - description: - 'An observer is defined as a special network, security, or application\ndevice used to detect, observe, or create network, security, or application-related\nevents and metrics.\n\nThis could be a custom hardware appliance or a server that has been configured\nto run special network, security, or application software. Examples include\nfirewalls, web proxies, intrusion detection/prevention systems, network monitoring\nsensors, web application firewalls, data loss prevention systems, and APM servers.\nThe observer.* fields shall be populated with details of the system, if any,\nthat detects, observes and/or creates a network, security, or application event\nor metric. Message queues and ETL components used in processing events or metrics\nare not considered observers in ECS.', - type: 'group', - fields: [ - { - name: 'egress', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'Observer.egress holds information like interface number and name,\nvlan, and zone information to classify egress traffic. Single armed monitoring\nsuch as a network sensor on a span port should only use observer.ingress\nto categorize traffic.', - default_field: false, - }, - { - name: 'egress.interface.alias', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Interface alias as reported by the system, typically used in firewall\nimplementations for e.g. inside, outside, or dmz logical interface naming.', - example: 'outside', - default_field: false, - }, - { - name: 'egress.interface.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface ID as reported by an observer (typically SNMP interface\nID).', - example: 10, - default_field: false, - }, - { - name: 'egress.interface.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface name as reported by the system.', - example: 'eth0', - default_field: false, - }, - { - name: 'egress.vlan.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'egress.vlan.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - { - name: 'egress.zone', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Network zone of outbound traffic as reported by the observer to\ncategorize the destination area of egress traffic, e.g. Internal, External,\nDMZ, HR, Legal, etc.', - example: 'Public_Internet', - default_field: false, - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'hostname', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Hostname of the observer.', - }, - { - name: 'ingress', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'Observer.ingress holds information like interface number and name,\nvlan, and zone information to classify ingress traffic. Single armed monitoring\nsuch as a network sensor on a span port should only use observer.ingress\nto categorize traffic.', - default_field: false, - }, - { - name: 'ingress.interface.alias', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Interface alias as reported by the system, typically used in firewall\nimplementations for e.g. inside, outside, or dmz logical interface naming.', - example: 'outside', - default_field: false, - }, - { - name: 'ingress.interface.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface ID as reported by an observer (typically SNMP interface\nID).', - example: 10, - default_field: false, - }, - { - name: 'ingress.interface.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface name as reported by the system.', - example: 'eth0', - default_field: false, - }, - { - name: 'ingress.vlan.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'ingress.vlan.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - { - name: 'ingress.zone', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Network zone of incoming traffic as reported by the observer to\ncategorize the source area of ingress traffic. e.g. internal, External, DMZ,\nHR, Legal, etc.', - example: 'DMZ', - default_field: false, - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: 'IP addresses of the observer.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC addresses of the observer', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Custom name of the observer.\n\nThis is a name that can be given to an observer. This can be helpful for example\nif multiple firewalls of the same model are used in an organization.\n\nIf no custom name is needed, the field can be left empty.', - example: '1_proxySG', - }, - { - name: 'os.family', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'os.full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'os.kernel', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'os.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'os.platform', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'os.version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - { - name: 'product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The product name of the observer.', - example: 's200', - }, - { - name: 'serial_number', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Observer serial number.', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of the observer the data is coming from.\n\nThere is no predefined list of observer types. Some examples are `forwarder`,\n`firewall`, `ids`, `ips`, `proxy`, `poller`, `sensor`, `APM server`.', - example: 'firewall', - }, - { - name: 'vendor', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Vendor name of the observer.', - example: 'Symantec', - }, - { - name: 'version', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Observer version.', - }, - ], - }, - { - name: 'organization', - title: 'Organization', - group: 2, - description: - 'The organization fields enrich data with information about the company\nor entity the data is associated with.\n\nThese fields help you arrange or filter data stored in an index by one or multiple\norganizations.', - type: 'group', - fields: [ - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the organization.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - }, - ], - }, - { - name: 'os', - title: 'Operating System', - group: 2, - description: 'The OS fields contain information about the operating system.', - type: 'group', - fields: [ - { - name: 'family', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'kernel', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'platform', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - ], - }, - { - name: 'package', - title: 'Package', - group: 2, - description: - 'These fields contain information about an installed software package.\nIt contains general information about a package, such as name, version or size.\nIt also contains installation details, such as time or location.', - type: 'group', - fields: [ - { - name: 'architecture', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Package architecture.', - example: 'x86_64', - }, - { - name: 'build_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the build version of the installed\npackage.\n\nFor example use the commit SHA of a non-released package.', - example: '36f4f7e89dd61b0988b12ee000b98966867710cd', - default_field: false, - }, - { - name: 'checksum', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Checksum of the installed package for verification.', - example: '68b329da9893e34099c7d8ad5cb9c940', - }, - { - name: 'description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Description of the package.', - example: - 'Open source programming language to build simple/reliable/efficient\nsoftware.', - }, - { - name: 'install_scope', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Indicating how the package was installed, e.g. user-local, global.', - example: 'global', - }, - { - name: 'installed', - level: 'extended', - type: 'date', - description: 'Time when package was installed.', - }, - { - name: 'license', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'License under which the package was released.\n\nUse a short name, e.g. the license identifier from SPDX License List where\npossible (https://spdx.org/licenses/).', - example: 'Apache License 2.0', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Package name', - example: 'go', - }, - { - name: 'path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Path where the package is installed.', - example: '/usr/local/Cellar/go/1.12.9/', - }, - { - name: 'reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Home page or reference URL of the software in this package, if\navailable.', - example: 'https://golang.org', - default_field: false, - }, - { - name: 'size', - level: 'extended', - type: 'long', - format: 'string', - description: 'Package size in bytes.', - example: 62231, - }, - { - name: 'type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Type of package.\n\nThis should contain the package file type, rather than the package manager\nname. Examples: rpm, dpkg, brew, npm, gem, nupkg, jar.', - example: 'rpm', - default_field: false, - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Package version', - example: '1.12.9', - }, - ], - }, - { - name: 'pe', - title: 'PE Header', - group: 2, - description: 'These fields contain Windows Portable Executable (PE) metadata.', - type: 'group', - fields: [ - { - name: 'company', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal company name of the file, provided at compile-time.', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal description of the file, provided at compile-time.', - example: 'Paint', - default_field: false, - }, - { - name: 'file_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal version of the file, provided at compile-time.', - example: '6.3.9600.17415', - default_field: false, - }, - { - name: 'original_file_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal name of the file, provided at compile-time.', - example: 'MSPAINT.EXE', - default_field: false, - }, - { - name: 'product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal product name of the file, provided at compile-time.', - example: 'Microsoft® Windows® Operating System', - default_field: false, - }, - ], - }, - { - name: 'process', - title: 'Process', - group: 2, - description: - 'These fields contain information about a process.\n\nThese fields can help you correlate metrics information with a process id/name\nfrom a log message. The `process.pid` often stays in the metric itself and\nis copied to the global field for correlation.', - type: 'group', - fields: [ - { - name: 'args', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.', - example: ['/usr/bin/ssh', '-l', 'user', '10.0.0.16'], - }, - { - name: 'args_count', - level: 'extended', - type: 'long', - description: - 'Length of the process.args array.\n\nThis field can be useful for querying or performing bucket analysis on how\nmany arguments were provided to start a process. More arguments may be an\nindication of suspicious activity.', - example: 4, - default_field: false, - }, - { - name: 'code_signature.exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'code_signature.subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'code_signature.trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - { - name: 'command_line', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: - 'Full command line that started the process, including the absolute\npath to the executable, and all arguments.\n\nSome arguments may be filtered to protect sensitive information.', - example: '/usr/bin/ssh -l user 10.0.0.16', - default_field: false, - }, - { - name: 'entity_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier for the process.\n\nThe implementation of this is specified by the data source, but some examples\nof what could be used here are a process-generated UUID, Sysmon Process GUIDs,\nor a hash of some uniquely identifying components of a process.\n\nConstructing a globally unique identifier is a common practice to mitigate\nPID reuse as well as to identify a specific process over time, across multiple\nmonitored hosts.', - example: 'c2c455d9f99375d', - default_field: false, - }, - { - name: 'executable', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Absolute path to the process executable.', - example: '/usr/bin/ssh', - }, - { - name: 'exit_code', - level: 'extended', - type: 'long', - description: - 'The exit code of the process, if this is a termination event.\n\nThe field should be absent if there is no exit code for the event (e.g. process\nstart).', - example: 137, - default_field: false, - }, - { - name: 'hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - }, - { - name: 'hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - }, - { - name: 'hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - }, - { - name: 'hash.sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Process name.\n\nSometimes called program name or similar.', - example: 'ssh', - }, - { - name: 'parent.args', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of process arguments.\n\nMay be filtered to protect sensitive information.', - example: ['ssh', '-l', 'user', '10.0.0.16'], - default_field: false, - }, - { - name: 'parent.args_count', - level: 'extended', - type: 'long', - description: - 'Length of the process.args array.\n\nThis field can be useful for querying or performing bucket analysis on how\nmany arguments were provided to start a process. More arguments may be an\nindication of suspicious activity.', - example: 4, - default_field: false, - }, - { - name: 'parent.code_signature.exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'parent.code_signature.status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'parent.code_signature.subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'parent.code_signature.trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'parent.code_signature.valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - { - name: 'parent.command_line', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: - 'Full command line that started the process, including the absolute\npath to the executable, and all arguments.\n\nSome arguments may be filtered to protect sensitive information.', - example: '/usr/bin/ssh -l user 10.0.0.16', - default_field: false, - }, - { - name: 'parent.entity_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier for the process.\n\nThe implementation of this is specified by the data source, but some examples\nof what could be used here are a process-generated UUID, Sysmon Process GUIDs,\nor a hash of some uniquely identifying components of a process.\n\nConstructing a globally unique identifier is a common practice to mitigate\nPID reuse as well as to identify a specific process over time, across multiple\nmonitored hosts.', - example: 'c2c455d9f99375d', - default_field: false, - }, - { - name: 'parent.executable', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: 'Absolute path to the process executable.', - example: '/usr/bin/ssh', - default_field: false, - }, - { - name: 'parent.exit_code', - level: 'extended', - type: 'long', - description: - 'The exit code of the process, if this is a termination event.\n\nThe field should be absent if there is no exit code for the event (e.g. process\nstart).', - example: 137, - default_field: false, - }, - { - name: 'parent.hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - default_field: false, - }, - { - name: 'parent.hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - default_field: false, - }, - { - name: 'parent.hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - default_field: false, - }, - { - name: 'parent.hash.sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - default_field: false, - }, - { - name: 'parent.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: 'Process name.\n\nSometimes called program name or similar.', - example: 'ssh', - default_field: false, - }, - { - name: 'parent.pgid', - level: 'extended', - type: 'long', - format: 'string', - description: 'Identifier of the group of processes the process belongs to.', - default_field: false, - }, - { - name: 'parent.pid', - level: 'core', - type: 'long', - format: 'string', - description: 'Process id.', - example: 4242, - default_field: false, - }, - { - name: 'parent.ppid', - level: 'extended', - type: 'long', - format: 'string', - description: "Parent process' pid.", - example: 4241, - default_field: false, - }, - { - name: 'parent.start', - level: 'extended', - type: 'date', - description: 'The time the process started.', - example: '2016-05-23T08:05:34.853Z', - default_field: false, - }, - { - name: 'parent.thread.id', - level: 'extended', - type: 'long', - format: 'string', - description: 'Thread ID.', - example: 4242, - default_field: false, - }, - { - name: 'parent.thread.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Thread name.', - example: 'thread-0', - default_field: false, - }, - { - name: 'parent.title', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: - 'Process title.\n\nThe proctitle, some times the same as process name. Can also be different:\nfor example a browser setting its title to the web page currently opened.', - default_field: false, - }, - { - name: 'parent.uptime', - level: 'extended', - type: 'long', - description: 'Seconds the process has been up.', - example: 1325, - default_field: false, - }, - { - name: 'parent.working_directory', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: 'The working directory of the process.', - example: '/home/alice', - default_field: false, - }, - { - name: 'pe.company', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal company name of the file, provided at compile-time.', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'pe.description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal description of the file, provided at compile-time.', - example: 'Paint', - default_field: false, - }, - { - name: 'pe.file_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal version of the file, provided at compile-time.', - example: '6.3.9600.17415', - default_field: false, - }, - { - name: 'pe.original_file_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal name of the file, provided at compile-time.', - example: 'MSPAINT.EXE', - default_field: false, - }, - { - name: 'pe.product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal product name of the file, provided at compile-time.', - example: 'Microsoft® Windows® Operating System', - default_field: false, - }, - { - name: 'pgid', - level: 'extended', - type: 'long', - format: 'string', - description: 'Identifier of the group of processes the process belongs to.', - }, - { - name: 'pid', - level: 'core', - type: 'long', - format: 'string', - description: 'Process id.', - example: 4242, - }, - { - name: 'ppid', - level: 'extended', - type: 'long', - format: 'string', - description: "Parent process' pid.", - example: 4241, - }, - { - name: 'start', - level: 'extended', - type: 'date', - description: 'The time the process started.', - example: '2016-05-23T08:05:34.853Z', - }, - { - name: 'thread.id', - level: 'extended', - type: 'long', - format: 'string', - description: 'Thread ID.', - example: 4242, - }, - { - name: 'thread.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Thread name.', - example: 'thread-0', - }, - { - name: 'title', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'Process title.\n\nThe proctitle, some times the same as process name. Can also be different:\nfor example a browser setting its title to the web page currently opened.', - }, - { - name: 'uptime', - level: 'extended', - type: 'long', - description: 'Seconds the process has been up.', - example: 1325, - }, - { - name: 'working_directory', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'The working directory of the process.', - example: '/home/alice', - }, - ], - }, - { - name: 'registry', - title: 'Registry', - group: 2, - description: 'Fields related to Windows Registry operations.', - type: 'group', - fields: [ - { - name: 'data.bytes', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Original bytes written with base64 encoding.\n\nFor Windows registry operations, such as SetValueEx and RegQueryValueEx, this\ncorresponds to the data pointed by `lp_data`. This is optional but provides\nbetter recoverability and should be populated for REG_BINARY encoded values.', - example: 'ZQBuAC0AVQBTAAAAZQBuAAAAAAA=', - default_field: false, - }, - { - name: 'data.strings', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Content when writing string types.\n\nPopulated as an array when writing string data to the registry. For single\nstring registry types (REG_SZ, REG_EXPAND_SZ), this should be an array with\none string. For sequences of string with REG_MULTI_SZ, this array will be\nvariable length. For numeric data, such as REG_DWORD and REG_QWORD, this should\nbe populated with the decimal representation (e.g `"1"`).', - example: '["C:\\rta\\red_ttp\\bin\\myapp.exe"]', - default_field: false, - }, - { - name: 'data.type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Standard registry type for encoding contents', - example: 'REG_SZ', - default_field: false, - }, - { - name: 'hive', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Abbreviated name for the hive.', - example: 'HKLM', - default_field: false, - }, - { - name: 'key', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Hive-relative path of keys.', - example: - 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\winword.exe', - default_field: false, - }, - { - name: 'path', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Full path, including hive, key and value', - example: - 'HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution\nOptions\\winword.exe\\Debugger', - default_field: false, - }, - { - name: 'value', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the value written.', - example: 'Debugger', - default_field: false, - }, - ], - }, - { - name: 'related', - title: 'Related', - group: 2, - description: - 'This field set is meant to facilitate pivoting around a piece of\ndata.\n\nSome pieces of information can be seen in many places in an ECS event. To facilitate\nsearching for them, store an array of all seen values to their corresponding\nfield in `related.`.\n\nA concrete example is IP addresses, which can be under host, observer, source,\ndestination, client, server, and network.forwarded_ip. If you append all IPs\nto `related.ip`, you can then search for a given IP trivially, no matter where\nit appeared, by querying `related.ip:192.0.2.15`.', - type: 'group', - fields: [ - { - name: 'hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - "All the hashes seen on your event. Populating this field, then\nusing it to search for hashes can help in situations where you're unsure what\nthe hash algorithm is (and therefore which key name to search).", - default_field: false, - }, - { - name: 'ip', - level: 'extended', - type: 'ip', - description: 'All of the IPs seen on your event.', - }, - { - name: 'user', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'All the user names seen on your event.', - default_field: false, - }, - ], - }, - { - name: 'rule', - title: 'Rule', - group: 2, - description: - 'Rule fields are used to capture the specifics of any observer or\nagent rules that generate alerts or other notable events.\n\nExamples of data sources that would populate the rule fields include: network\nadmission control platforms, network or host IDS/IPS, network firewalls, web\napplication firewalls, url filters, endpoint detection and response (EDR) systems,\netc.', - type: 'group', - fields: [ - { - name: 'author', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name, organization, or pseudonym of the author or authors who created\nthe rule used to generate this event.', - example: ['Star-Lord'], - default_field: false, - }, - { - name: 'category', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A categorization value keyword used by the entity using the rule\nfor detection of this event.', - example: 'Attempted Information Leak', - default_field: false, - }, - { - name: 'description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The description of the rule generating the event.', - example: 'Block requests to public DNS over HTTPS / TLS protocols', - default_field: false, - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A rule ID that is unique within the scope of an agent, observer,\nor other entity using the rule for detection of this event.', - example: 101, - default_field: false, - }, - { - name: 'license', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the license under which the rule used to generate this\nevent is made available.', - example: 'Apache 2.0', - default_field: false, - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The name of the rule or signature generating the event.', - example: 'BLOCK_DNS_over_TLS', - default_field: false, - }, - { - name: 'reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Reference URL to additional information about the rule used to\ngenerate this event.\n\nThe URL can point to the vendor documentation about the rule. If that is\nnot available, it can also be a link to a more general page describing this\ntype of alert.', - example: 'https://en.wikipedia.org/wiki/DNS_over_TLS', - default_field: false, - }, - { - name: 'ruleset', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the ruleset, policy, group, or parent category in which\nthe rule used to generate this event is a member.', - example: 'Standard_Protocol_Filters', - default_field: false, - }, - { - name: 'uuid', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A rule ID that is unique within the scope of a set or group of\nagents, observers, or other entities using the rule for detection of this\nevent.', - example: 1100110011, - default_field: false, - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The version / revision of the rule being used for analysis.', - example: 1.1, - default_field: false, - }, - ], - }, - { - name: 'server', - title: 'Server', - group: 2, - description: - 'A Server is defined as the responder in a network connection for\nevents regarding sessions, connections, or bidirectional flow records.\n\nFor TCP events, the server is the receiver of the initial SYN packet(s) of the\nTCP connection. For other protocols, the server is generally the responder in\nthe network transaction. Some systems actually use the term "responder" to refer\nthe server in TCP connections. The server fields describe details about the\nsystem acting as the server in the network event. Server fields are usually\npopulated in conjunction with client fields. Server fields are generally not\npopulated for packet-level events.\n\nClient / server representations can add semantic context to an exchange, which\nis helpful to visualize the data in certain situations. If your context falls\nin that category, you should still ensure that source and destination are filled\nappropriately.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Some event server addresses are defined ambiguously. The event\nwill sometimes list an IP, a domain or a unix socket. You should always store\nthe raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'as.number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'as.organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the server to the client.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Server domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: - 'IP address of the server.\n\nCan be one or multiple IPv4 or IPv6 addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC address of the server.', - }, - { - name: 'nat.ip', - level: 'extended', - type: 'ip', - description: - 'Translated ip of destination based NAT sessions (e.g. internet\nto private DMZ)\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'nat.port', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Translated port of destination based NAT sessions (e.g. internet\nto private DMZ)\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the server to the client.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the server.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered server domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'service', - title: 'Service', - group: 2, - description: - 'The service fields describe the service for or from which the data\nwas collected.\n\nThese fields help you find and correlate logs for a specific service and version.', - type: 'group', - fields: [ - { - name: 'ephemeral_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Ephemeral identifier of this service (if one exists).\n\nThis id normally changes across restarts, but `service.id` does not.', - example: '8a4f500f', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier of the running service. If the service is comprised\nof many nodes, the `service.id` should be the same for all nodes.\n\nThis id should uniquely identify the service. This makes it possible to correlate\nlogs and metrics for one specific service, no matter which particular node\nemitted the event.\n\nNote that if you need to see the events from one specific host of the service,\nyou should filter on that `host.name` or `host.id` instead.', - example: 'd37e5ebfe0ae6c4972dbe9f0174a1637bb8247f6', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the service data is collected from.\n\nThe name of the service is normally user given. This allows for distributed\nservices that run on multiple hosts to correlate the related instances based\non the name.\n\nIn the case of Elasticsearch the `service.name` could contain the cluster\nname. For Beats the `service.name` is by default a copy of the `service.type`\nfield if no name is specified.', - example: 'elasticsearch-metrics', - }, - { - name: 'node.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of a service node.\n\nThis allows for two nodes of the same service running on the same host to\nbe differentiated. Therefore, `service.node.name` should typically be unique\nacross nodes of a given service.\n\nIn the case of Elasticsearch, the `service.node.name` could contain the unique\nnode name within the Elasticsearch cluster. In cases where the service does not\nhave the concept of a node name, the host name or container name can be used\nto distinguish running instances that make up this service. If those do not\nprovide uniqueness (e.g. multiple instances of the service running on the\nsame host) - the node name can be manually set.', - example: 'instance-0000000016', - }, - { - name: 'state', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Current state of the service.', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of the service data is collected from.\n\nThe type can be used to group and correlate logs and metrics from one service\ntype.\n\nExample: If logs or metrics are collected from Elasticsearch, `service.type`\nwould be `elasticsearch`.', - example: 'elasticsearch', - }, - { - name: 'version', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Version of the service the data was collected from.\n\nThis allows to look at a data set only for a specific version of a service.', - example: '3.2.4', - }, - ], - }, - { - name: 'source', - title: 'Source', - group: 2, - description: - 'Source fields describe details about the source of a packet/event.\n\nSource fields are usually populated in conjunction with destination fields.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Some event source addresses are defined ambiguously. The event\nwill sometimes list an IP, a domain or a unix socket. You should always store\nthe raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'as.number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'as.organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the source to the destination.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Source domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: - 'IP address of the source.\n\nCan be one or multiple IPv4 or IPv6 addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC address of the source.', - }, - { - name: 'nat.ip', - level: 'extended', - type: 'ip', - description: - 'Translated ip of source based NAT sessions (e.g. internal client\nto internet)\n\nTypically connections traversing load balancers, firewalls, or routers.', - }, - { - name: 'nat.port', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Translated port of source based NAT sessions. (e.g. internal client\nto internet)\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the source to the destination.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the source.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered source domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'threat', - title: 'Threat', - group: 2, - description: - 'Fields to classify events and alerts according to a threat taxonomy\nsuch as the Mitre ATT&CK framework.\n\nThese fields are for users to classify alerts from all of their sources (e.g.\nIDS, NGFW, etc.) within a common taxonomy. The threat.tactic.* are meant to\ncapture the high level category of the threat (e.g. "impact"). The threat.technique.*\nfields are meant to capture which kind of approach is used by this detected\nthreat, to accomplish the goal (e.g. "endpoint denial of service").', - type: 'group', - fields: [ - { - name: 'framework', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the threat framework used to further categorize and classify\nthe tactic and technique of the reported threat. Framework classification\ncan be provided by detecting systems, evaluated at ingest time, or retrospectively\ntagged to events.', - example: 'MITRE ATT&CK', - }, - { - name: 'tactic.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The id of tactic used by this threat. You can use the Mitre ATT&CK\nMatrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/\n)', - example: 'TA0040', - }, - { - name: 'tactic.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the type of tactic used by this threat. You can use the\nMitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/\n)', - example: 'impact', - }, - { - name: 'tactic.reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The reference url of tactic used by this threat. You can use the\nMitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/\n)', - example: 'https://attack.mitre.org/tactics/TA0040/', - }, - { - name: 'technique.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The id of technique used by this tactic. You can use the Mitre\nATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/\n)', - example: 'T1499', - }, - { - name: 'technique.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'The name of technique used by this tactic. You can use the Mitre\nATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/\n)', - example: 'endpoint denial of service', - }, - { - name: 'technique.reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The reference url of technique used by this tactic. You can use\nthe Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/\n)', - example: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - { - name: 'tls', - title: 'TLS', - group: 2, - description: - 'Fields related to a TLS connection. These fields focus on the TLS\nprotocol itself and intentionally avoids in-depth analysis of the related x.509\ncertificate files.', - type: 'group', - fields: [ - { - name: 'cipher', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'String indicating the cipher used during the current connection.', - example: 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256', - default_field: false, - }, - { - name: 'client.certificate', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'PEM-encoded stand-alone certificate offered by the client. This\nis usually mutually-exclusive of `client.certificate_chain` since this value\nalso exists in that list.', - example: 'MII...', - default_field: false, - }, - { - name: 'client.certificate_chain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of PEM-encoded certificates that make up the certificate\nchain offered by the client. This is usually mutually-exclusive of `client.certificate`\nsince that value should be the first certificate in the chain.', - example: ['MII...', 'MII...'], - default_field: false, - }, - { - name: 'client.hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the MD5 digest of DER-encoded version\nof certificate offered by the client. For consistency with other hash values,\nthis value should be formatted as an uppercase hash.', - example: '0F76C7F2C55BFD7D8E8B8F4BFBF0C9EC', - default_field: false, - }, - { - name: 'client.hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the SHA1 digest of DER-encoded version\nof certificate offered by the client. For consistency with other hash values,\nthis value should be formatted as an uppercase hash.', - example: '9E393D93138888D288266C2D915214D1D1CCEB2A', - default_field: false, - }, - { - name: 'client.hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the SHA256 digest of DER-encoded\nversion of certificate offered by the client. For consistency with other hash\nvalues, this value should be formatted as an uppercase hash.', - example: '0687F666A054EF17A08E2F2162EAB4CBC0D265E1D7875BE74BF3C712CA92DAF0', - default_field: false, - }, - { - name: 'client.issuer', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Distinguished name of subject of the issuer of the x.509 certificate\npresented by the client.', - example: 'CN=MyDomain Root CA, OU=Infrastructure Team, DC=mydomain, DC=com', - default_field: false, - }, - { - name: 'client.ja3', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A hash that identifies clients based on how they perform an SSL/TLS\nhandshake.', - example: 'd4e5b18d6b55c71272893221c96ba240', - default_field: false, - }, - { - name: 'client.not_after', - level: 'extended', - type: 'date', - description: - 'Date/Time indicating when client certificate is no longer considered\nvalid.', - example: '2021-01-01T00:00:00.000Z', - default_field: false, - }, - { - name: 'client.not_before', - level: 'extended', - type: 'date', - description: 'Date/Time indicating when client certificate is first considered\nvalid.', - example: '1970-01-01T00:00:00.000Z', - default_field: false, - }, - { - name: 'client.server_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Also called an SNI, this tells the server which hostname to which\nthe client is attempting to connect. When this value is available, it should\nget copied to `destination.domain`.', - example: 'www.elastic.co', - default_field: false, - }, - { - name: 'client.subject', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Distinguished name of subject of the x.509 certificate presented\nby the client.', - example: 'CN=myclient, OU=Documentation Team, DC=mydomain, DC=com', - default_field: false, - }, - { - name: 'client.supported_ciphers', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Array of ciphers offered by the client during the client hello.', - example: [ - 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384', - 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384', - '...', - ], - default_field: false, - }, - { - name: 'curve', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'String indicating the curve used for the given cipher, when applicable.', - example: 'secp256r1', - default_field: false, - }, - { - name: 'established', - level: 'extended', - type: 'boolean', - description: - 'Boolean flag indicating if the TLS negotiation was successful and\ntransitioned to an encrypted tunnel.', - default_field: false, - }, - { - name: 'next_protocol', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'String indicating the protocol being tunneled. Per the values in\nthe IANA registry (https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids),\nthis string should be lower case.', - example: 'http/1.1', - default_field: false, - }, - { - name: 'resumed', - level: 'extended', - type: 'boolean', - description: - 'Boolean flag indicating if this TLS connection was resumed from\nan existing TLS negotiation.', - default_field: false, - }, - { - name: 'server.certificate', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'PEM-encoded stand-alone certificate offered by the server. This\nis usually mutually-exclusive of `server.certificate_chain` since this value\nalso exists in that list.', - example: 'MII...', - default_field: false, - }, - { - name: 'server.certificate_chain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of PEM-encoded certificates that make up the certificate\nchain offered by the server. This is usually mutually-exclusive of `server.certificate`\nsince that value should be the first certificate in the chain.', - example: ['MII...', 'MII...'], - default_field: false, - }, - { - name: 'server.hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the MD5 digest of DER-encoded version\nof certificate offered by the server. For consistency with other hash values,\nthis value should be formatted as an uppercase hash.', - example: '0F76C7F2C55BFD7D8E8B8F4BFBF0C9EC', - default_field: false, - }, - { - name: 'server.hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the SHA1 digest of DER-encoded version\nof certificate offered by the server. For consistency with other hash values,\nthis value should be formatted as an uppercase hash.', - example: '9E393D93138888D288266C2D915214D1D1CCEB2A', - default_field: false, - }, - { - name: 'server.hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the SHA256 digest of DER-encoded\nversion of certificate offered by the server. For consistency with other hash\nvalues, this value should be formatted as an uppercase hash.', - example: '0687F666A054EF17A08E2F2162EAB4CBC0D265E1D7875BE74BF3C712CA92DAF0', - default_field: false, - }, - { - name: 'server.issuer', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Subject of the issuer of the x.509 certificate presented by the\nserver.', - example: 'CN=MyDomain Root CA, OU=Infrastructure Team, DC=mydomain, DC=com', - default_field: false, - }, - { - name: 'server.ja3s', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A hash that identifies servers based on how they perform an SSL/TLS\nhandshake.', - example: '394441ab65754e2207b1e1b457b3641d', - default_field: false, - }, - { - name: 'server.not_after', - level: 'extended', - type: 'date', - description: - 'Timestamp indicating when server certificate is no longer considered\nvalid.', - example: '2021-01-01T00:00:00.000Z', - default_field: false, - }, - { - name: 'server.not_before', - level: 'extended', - type: 'date', - description: 'Timestamp indicating when server certificate is first considered\nvalid.', - example: '1970-01-01T00:00:00.000Z', - default_field: false, - }, - { - name: 'server.subject', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Subject of the x.509 certificate presented by the server.', - example: 'CN=www.mydomain.com, OU=Infrastructure Team, DC=mydomain, DC=com', - default_field: false, - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Numeric part of the version parsed from the original string.', - example: '1.2', - default_field: false, - }, - { - name: 'version_protocol', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Normalized lowercase protocol name parsed from original string.', - example: 'tls', - default_field: false, - }, - ], - }, - { - name: 'tracing', - title: 'Tracing', - group: 2, - description: - 'Distributed tracing makes it possible to analyze performance throughout\na microservice architecture all in one view. This is accomplished by tracing\nall of the requests - from the initial web request in the front-end service\n- to queries made through multiple back-end services.', - type: 'group', - fields: [ - { - name: 'trace.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier of the trace.\n\nA trace groups multiple events like transactions that belong together. For\nexample, a user request handled by multiple inter-connected services.', - example: '4bf92f3577b34da6a3ce929d0e0e4736', - }, - { - name: 'transaction.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier of the transaction.\n\nA transaction is the highest level of work measured within a service, such\nas a request to a server.', - example: '00f067aa0ba902b7', - }, - ], - }, - { - name: 'url', - title: 'URL', - group: 2, - description: - 'URL fields provide support for complete or partial URLs, and supports\nthe breaking down into scheme, domain, path, and so on.', - type: 'group', - fields: [ - { - name: 'domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Domain of the url, such as "www.elastic.co".\n\nIn some cases a URL may refer to an IP and/or port directly, without a domain\nname. In this case, the IP address would go to the `domain` field.', - example: 'www.elastic.co', - }, - { - name: 'extension', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The field contains the file extension from the original request\nurl.\n\nThe file extension is only set if it exists, as not every url has a file extension.\n\nThe leading period must not be included. For example, the value must be "png",\nnot ".png".', - example: 'png', - }, - { - name: 'fragment', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Portion of the url after the `#`, such as "top".\n\nThe `#` is not part of the fragment.', - }, - { - name: 'full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'If full URLs are important to your use case, they should be stored\nin `url.full`, whether this field is reconstructed or present in the event\nsource.', - example: 'https://www.elastic.co:443/search?q=elasticsearch#top', - }, - { - name: 'original', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'Unmodified original url as seen in the event source.\n\nNote that in network monitoring, the observed URL may be a full URL, whereas\nin access logs, the URL is often just represented as a path.\n\nThis field is meant to represent the URL as it was observed, complete or not.', - example: - 'https://www.elastic.co:443/search?q=elasticsearch#top or /search?q=elasticsearch', - }, - { - name: 'password', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Password of the request.', - }, - { - name: 'path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Path of the request, such as "/search".', - }, - { - name: 'port', - level: 'extended', - type: 'long', - format: 'string', - description: 'Port of the request, such as 443.', - example: 443, - }, - { - name: 'query', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The query field describes the query string of the request, such\nas "q=elasticsearch".\n\nThe `?` is excluded from the query string. If a URL contains no `?`, there\nis no query field. If there is a `?` but no query, the query field exists\nwith an empty string. The `exists` query can be used to differentiate between\nthe two cases.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered url domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'scheme', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Scheme of the request, such as "https".\n\nNote: The `:` is not part of the scheme.', - example: 'https', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'username', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Username of the request.', - }, - ], - }, - { - name: 'user', - title: 'User', - group: 2, - description: - 'The user fields describe information about the user that is relevant\nto the event.\n\nFields can have one entry or multiple entries. If a user has more than one id,\nprovide an array that includes all of them.', - type: 'group', - fields: [ - { - name: 'domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifiers of the user.', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'user_agent', - title: 'User agent', - group: 2, - description: - 'The user_agent fields normally come from a browser request.\n\nThey often show up in web service logs coming from the parsed user agent string.', - type: 'group', - fields: [ - { - name: 'device.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the device.', - example: 'iPhone', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the user agent.', - example: 'Safari', - }, - { - name: 'original', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: 'Unparsed user_agent string.', - example: - 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_1 like Mac OS X) AppleWebKit/605.1.15\n(KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1', - }, - { - name: 'os.family', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'os.full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'os.kernel', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'os.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'os.platform', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'os.version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Version of the user agent.', - example: 12, - }, - ], - }, - { - name: 'vlan', - title: 'VLAN', - group: 2, - description: - 'The VLAN fields are used to identify 802.1q tag(s) of a packet,\nas well as ingress and egress VLAN associations of an observer in relation to\na specific packet or connection.\n\nNetwork.vlan fields are used to record a single VLAN tag, or the outer tag in\nthe case of q-in-q encapsulations, for a packet or connection as observed, typically\nprovided by a network sensor (e.g. Zeek, Wireshark) passively reporting on traffic.\n\nNetwork.inner VLAN fields are used to report inner q-in-q 802.1q tags (multiple\n802.1q encapsulations) as observed, typically provided by a network sensor (e.g.\nZeek, Wireshark) passively reporting on traffic. Network.inner VLAN fields should\nonly be used in addition to network.vlan fields to indicate q-in-q tagging.\n\nObserver.ingress and observer.egress VLAN values are used to record observer\nspecific information when observer events contain discrete ingress and egress\nVLAN information, typically provided by firewalls, routers, or load balancers.', - type: 'group', - fields: [ - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - ], - }, - { - name: 'vulnerability', - title: 'Vulnerability', - group: 2, - description: - 'The vulnerability fields describe information about a vulnerability\nthat is relevant to an event.', - type: 'group', - fields: [ - { - name: 'category', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of system or architecture that the vulnerability affects.\nThese may be platform-specific (for example, Debian or SUSE) or general (for\nexample, Database or Firewall). For example (https://qualysguard.qualys.com/qwebhelp/fo_portal/knowledgebase/vulnerability_categories.htm[Qualys\nvulnerability categories])\n\nThis field must be an array.', - example: '["Firewall"]', - default_field: false, - }, - { - name: 'classification', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The classification of the vulnerability scoring system. For example\n(https://www.first.org/cvss/)', - example: 'CVSS', - default_field: false, - }, - { - name: 'description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: - 'The description of the vulnerability that provides additional context\nof the vulnerability. For example (https://cve.mitre.org/about/faqs.html#cve_entry_descriptions_created[Common\nVulnerabilities and Exposure CVE description])', - example: 'In macOS before 2.12.6, there is a vulnerability in the RPC...', - default_field: false, - }, - { - name: 'enumeration', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of identifier used for this vulnerability. For example\n(https://cve.mitre.org/about/)', - example: 'CVE', - default_field: false, - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The identification (ID) is the number portion of a vulnerability\nentry. It includes a unique identification number for the vulnerability. For\nexample (https://cve.mitre.org/about/faqs.html#what_is_cve_id)[Common Vulnerabilities\nand Exposure CVE ID]', - example: 'CVE-2019-00001', - default_field: false, - }, - { - name: 'reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A resource that provides additional information, context, and mitigations\nfor the identified vulnerability.', - example: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-6111', - default_field: false, - }, - { - name: 'report_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The report or scan identification number.', - example: 20191018.0001, - default_field: false, - }, - { - name: 'scanner.vendor', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The name of the vulnerability scanner vendor.', - example: 'Tenable', - default_field: false, - }, - { - name: 'score.base', - level: 'extended', - type: 'float', - description: - 'Scores can range from 0.0 to 10.0, with 10.0 being the most severe.\n\nBase scores cover an assessment for exploitability metrics (attack vector,\ncomplexity, privileges, and user interaction), impact metrics (confidentiality,\nintegrity, and availability), and scope. For example (https://www.first.org/cvss/specification-document)', - example: 5.5, - default_field: false, - }, - { - name: 'score.environmental', - level: 'extended', - type: 'float', - description: - 'Scores can range from 0.0 to 10.0, with 10.0 being the most severe.\n\nEnvironmental scores cover an assessment for any modified Base metrics, confidentiality,\nintegrity, and availability requirements. For example (https://www.first.org/cvss/specification-document)', - example: 5.5, - default_field: false, - }, - { - name: 'score.temporal', - level: 'extended', - type: 'float', - description: - 'Scores can range from 0.0 to 10.0, with 10.0 being the most severe.\n\nTemporal scores cover an assessment for code maturity, remediation level,\nand confidence. For example (https://www.first.org/cvss/specification-document)', - default_field: false, - }, - { - name: 'score.version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The National Vulnerability Database (NVD) provides qualitative\nseverity rankings of "Low", "Medium", and "High" for CVSS v2.0 base score\nranges in addition to the severity ratings for CVSS v3.0 as they are defined\nin the CVSS v3.0 specification.\n\nCVSS is owned and managed by FIRST.Org, Inc. (FIRST), a US-based non-profit\norganization, whose mission is to help computer security incident response\nteams across the world. For example (https://nvd.nist.gov/vuln-metrics/cvss)', - example: 2, - default_field: false, - }, - { - name: 'severity', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The severity of the vulnerability can help with metrics and internal\nprioritization regarding remediation. For example (https://nvd.nist.gov/vuln-metrics/cvss)', - example: 'Critical', - default_field: false, - }, - ], - }, - ], - }, - { - key: 'beat', - anchor: 'beat-common', - title: 'Beat', - description: 'Contains common beat fields available in all event types.\n', - fields: [ - { - name: 'agent.hostname', - type: 'keyword', - description: 'Hostname of the agent.', - }, - { - name: 'beat.timezone', - type: 'alias', - path: 'event.timezone', - migration: true, - }, - { - name: 'fields', - type: 'object', - object_type: 'keyword', - description: 'Contains user configurable fields.\n', - }, - { - name: 'beat.name', - type: 'alias', - path: 'host.name', - migration: true, - }, - { - name: 'beat.hostname', - type: 'alias', - path: 'agent.hostname', - migration: true, - }, - { - name: 'timeseries.instance', - type: 'keyword', - description: 'Time series instance id', - }, - ], - }, - { - key: 'cloud', - title: 'Cloud provider metadata', - description: 'Metadata from cloud providers added by the add_cloud_metadata processor.\n', - fields: [ - { - name: 'cloud.project.id', - example: 'project-x', - description: 'Name of the project in Google Cloud.\n', - }, - { - name: 'cloud.image.id', - example: 'ami-abcd1234', - description: 'Image ID for the cloud instance.\n', - }, - { - name: 'meta.cloud.provider', - type: 'alias', - path: 'cloud.provider', - migration: true, - }, - { - name: 'meta.cloud.instance_id', - type: 'alias', - path: 'cloud.instance.id', - migration: true, - }, - { - name: 'meta.cloud.instance_name', - type: 'alias', - path: 'cloud.instance.name', - migration: true, - }, - { - name: 'meta.cloud.machine_type', - type: 'alias', - path: 'cloud.machine.type', - migration: true, - }, - { - name: 'meta.cloud.availability_zone', - type: 'alias', - path: 'cloud.availability_zone', - migration: true, - }, - { - name: 'meta.cloud.project_id', - type: 'alias', - path: 'cloud.project.id', - migration: true, - }, - { - name: 'meta.cloud.region', - type: 'alias', - path: 'cloud.region', - migration: true, - }, - ], - }, - { - key: 'docker', - title: 'Docker', - description: 'Docker stats collected from Docker.\n', - short_config: false, - anchor: 'docker-processor', - fields: [ - { - name: 'docker', - type: 'group', - fields: [ - { - name: 'container.id', - type: 'alias', - path: 'container.id', - migration: true, - }, - { - name: 'container.image', - type: 'alias', - path: 'container.image.name', - migration: true, - }, - { - name: 'container.name', - type: 'alias', - path: 'container.name', - migration: true, - }, - { - name: 'container.labels', - type: 'object', - object_type: 'keyword', - description: 'Image labels.\n', - }, - ], - }, - ], - }, - { - key: 'host', - title: 'Host', - description: 'Info collected for the host machine.\n', - anchor: 'host-processor', - fields: [ - { - name: 'host', - type: 'group', - fields: [ - { - name: 'containerized', - type: 'boolean', - description: 'If the host is a container.\n', - }, - { - name: 'os.build', - type: 'keyword', - example: '18D109', - description: 'OS build information.\n', - }, - { - name: 'os.codename', - type: 'keyword', - example: 'stretch', - description: 'OS codename, if any.\n', - }, - ], - }, - ], - }, - { - key: 'kubernetes', - title: 'Kubernetes', - description: 'Kubernetes metadata added by the kubernetes processor\n', - short_config: false, - anchor: 'kubernetes-processor', - fields: [ - { - name: 'kubernetes', - type: 'group', - fields: [ - { - name: 'pod.name', - type: 'keyword', - description: 'Kubernetes pod name\n', - }, - { - name: 'pod.uid', - type: 'keyword', - description: 'Kubernetes Pod UID\n', - }, - { - name: 'namespace', - type: 'keyword', - description: 'Kubernetes namespace\n', - }, - { - name: 'node.name', - type: 'keyword', - description: 'Kubernetes node name\n', - }, - { - name: 'labels.*', - type: 'object', - object_type: 'keyword', - object_type_mapping_type: '*', - description: 'Kubernetes labels map\n', - }, - { - name: 'annotations.*', - type: 'object', - object_type: 'keyword', - object_type_mapping_type: '*', - description: 'Kubernetes annotations map\n', - }, - { - name: 'replicaset.name', - type: 'keyword', - description: 'Kubernetes replicaset name\n', - }, - { - name: 'deployment.name', - type: 'keyword', - description: 'Kubernetes deployment name\n', - }, - { - name: 'statefulset.name', - type: 'keyword', - description: 'Kubernetes statefulset name\n', - }, - { - name: 'container.name', - type: 'keyword', - description: 'Kubernetes container name\n', - }, - { - name: 'container.image', - type: 'keyword', - description: 'Kubernetes container image\n', - }, - ], - }, - ], - }, - { - key: 'process', - title: 'Process', - description: 'Process metadata fields\n', - fields: [ - { - name: 'process', - type: 'group', - fields: [ - { - name: 'exe', - type: 'alias', - path: 'process.executable', - migration: true, - }, - ], - }, - ], - }, - { - key: 'jolokia-autodiscover', - title: 'Jolokia Discovery autodiscover provider', - description: 'Metadata from Jolokia Discovery added by the jolokia provider.\n', - fields: [ - { - name: 'jolokia.agent.version', - type: 'keyword', - description: 'Version number of jolokia agent.\n', - }, - { - name: 'jolokia.agent.id', - type: 'keyword', - description: - 'Each agent has a unique id which can be either provided during startup of the agent in form of a configuration parameter or being autodetected. If autodected, the id has several parts: The IP, the process id, hashcode of the agent and its type.\n', - }, - { - name: 'jolokia.server.product', - type: 'keyword', - description: 'The container product if detected.\n', - }, - { - name: 'jolokia.server.version', - type: 'keyword', - description: "The container's version (if detected).\n", - }, - { - name: 'jolokia.server.vendor', - type: 'keyword', - description: 'The vendor of the container the agent is running in.\n', - }, - { - name: 'jolokia.url', - type: 'keyword', - description: 'The URL how this agent can be contacted.\n', - }, - { - name: 'jolokia.secured', - type: 'boolean', - description: 'Whether the agent was configured for authentication or not.\n', - }, - ], - }, - { - key: 'log', - title: 'Log file content', - description: 'Contains log file lines.\n', - fields: [ - { - name: 'log.file.path', - type: 'keyword', - required: false, - description: - 'The file from which the line was read. This field contains the absolute path to the file. For example: `/var/log/system.log`.\n', - }, - { - name: 'log.source.address', - type: 'keyword', - required: false, - description: 'Source address from which the log event was read / sent from.\n', - }, - { - name: 'log.offset', - type: 'long', - required: false, - description: 'The file offset the reported line starts at.\n', - }, - { - name: 'stream', - type: 'keyword', - required: false, - description: "Log stream when reading container logs, can be 'stdout' or 'stderr'\n", - }, - { - name: 'input.type', - required: true, - description: - 'The input type from which the event was generated. This field is set to the value specified for the `type` option in the input section of the Filebeat config file.\n', - }, - { - name: 'syslog.facility', - type: 'long', - required: false, - description: 'The facility extracted from the priority.\n', - }, - { - name: 'syslog.priority', - type: 'long', - required: false, - description: 'The priority of the syslog event.\n', - }, - { - name: 'syslog.severity_label', - type: 'keyword', - required: false, - description: 'The human readable severity.\n', - }, - { - name: 'syslog.facility_label', - type: 'keyword', - required: false, - description: 'The human readable facility.\n', - }, - { - name: 'process.program', - type: 'keyword', - required: false, - description: 'The name of the program.\n', - }, - { - name: 'log.flags', - description: 'This field contains the flags of the event.\n', - }, - { - name: 'http.response.content_length', - type: 'alias', - path: 'http.response.body.bytes', - migration: true, - }, - { - name: 'user_agent', - type: 'group', - fields: [ - { - name: 'os', - type: 'group', - fields: [ - { - name: 'full_name', - type: 'keyword', - }, - ], - }, - ], - }, - { - name: 'fileset.name', - type: 'keyword', - description: 'The Filebeat fileset that generated this event.\n', - }, - { - name: 'fileset.module', - type: 'alias', - path: 'event.module', - migration: true, - }, - { - name: 'read_timestamp', - type: 'alias', - path: 'event.created', - migration: true, - }, - { - name: 'docker.attrs', - type: 'object', - object_type: 'keyword', - description: - "docker.attrs contains labels and environment variables written by docker's JSON File logging driver. These fields are only available when they are configured in the logging driver options.\n", - }, - { - name: 'icmp.code', - type: 'keyword', - description: 'ICMP code.\n', - }, - { - name: 'icmp.type', - type: 'keyword', - description: 'ICMP type.\n', - }, - { - name: 'igmp.type', - type: 'keyword', - description: 'IGMP type.\n', - }, - { - name: 'azure', - type: 'group', - fields: [ - { - name: 'eventhub', - type: 'keyword', - description: 'Name of the eventhub.\n', - }, - { - name: 'offset', - type: 'long', - description: 'The offset.\n', - }, - { - name: 'enqueued_time', - type: 'date', - description: 'The enqueued time.\n', - }, - { - name: 'partition_id', - type: 'long', - description: 'The partition id.\n', - }, - { - name: 'consumer_group', - type: 'keyword', - description: 'The consumer group.\n', - }, - { - name: 'sequence_number', - type: 'long', - description: 'The sequence number.\n', - }, - ], - }, - { - name: 'kafka', - type: 'group', - fields: [ - { - name: 'topic', - type: 'keyword', - description: 'Kafka topic\n', - }, - { - name: 'partition', - type: 'long', - description: 'Kafka partition number\n', - }, - { - name: 'offset', - type: 'long', - description: 'Kafka offset of this message\n', - }, - { - name: 'key', - type: 'keyword', - description: 'Kafka key, corresponding to the Kafka value stored in the message\n', - }, - { - name: 'block_timestamp', - type: 'date', - description: 'Kafka outer (compressed) block timestamp\n', - }, - { - name: 'headers', - type: 'array', - description: - 'An array of Kafka header strings for this message, in the form ": ".\n', - }, - ], - }, - ], - }, - { - key: 'apache', - title: 'Apache', - description: 'Apache Module\n', - short_config: true, - fields: [ - { - name: 'apache2', - type: 'group', - description: 'Aliases for backward compatibility with old apache2 fields\n', - fields: [ - { - name: 'access', - type: 'group', - fields: [ - { - name: 'remote_ip', - type: 'alias', - path: 'source.address', - migration: true, - }, - { - name: 'ssl.protocol', - type: 'alias', - path: 'apache.access.ssl.protocol', - migration: true, - }, - { - name: 'ssl.cipher', - type: 'alias', - path: 'apache.access.ssl.cipher', - migration: true, - }, - { - name: 'body_sent.bytes', - type: 'alias', - path: 'http.response.body.bytes', - migration: true, - }, - { - name: 'user_name', - type: 'alias', - path: 'user.name', - migration: true, - }, - { - name: 'method', - type: 'alias', - path: 'http.request.method', - migration: true, - }, - { - name: 'url', - type: 'alias', - path: 'url.original', - migration: true, - }, - { - name: 'http_version', - type: 'alias', - path: 'http.version', - migration: true, - }, - { - name: 'response_code', - type: 'alias', - path: 'http.response.status_code', - migration: true, - }, - { - name: 'referrer', - type: 'alias', - path: 'http.request.referrer', - migration: true, - }, - { - name: 'agent', - type: 'alias', - path: 'user_agent.original', - migration: true, - }, - { - name: 'user_agent', - type: 'group', - fields: [ - { - name: 'device', - type: 'alias', - path: 'user_agent.device.name', - migration: true, - }, - { - name: 'name', - type: 'alias', - path: 'user_agent.name', - migration: true, - }, - { - name: 'os', - type: 'alias', - path: 'user_agent.os.full_name', - migration: true, - }, - { - name: 'os_name', - type: 'alias', - path: 'user_agent.os.name', - migration: true, - }, - { - name: 'original', - type: 'alias', - path: 'user_agent.original', - migration: true, - }, - ], - }, - { - name: 'geoip', - type: 'group', - fields: [ - { - name: 'continent_name', - type: 'alias', - path: 'source.geo.continent_name', - migration: true, - }, - { - name: 'country_iso_code', - type: 'alias', - path: 'source.geo.country_iso_code', - migration: true, - }, - { - name: 'location', - type: 'alias', - path: 'source.geo.location', - migration: true, - }, - { - name: 'region_name', - type: 'alias', - path: 'source.geo.region_name', - migration: true, - }, - { - name: 'city_name', - type: 'alias', - path: 'source.geo.city_name', - migration: true, - }, - { - name: 'region_iso_code', - type: 'alias', - path: 'source.geo.region_iso_code', - migration: true, - }, - ], - }, - ], - }, - { - name: 'error', - type: 'group', - fields: [ - { - name: 'level', - type: 'alias', - path: 'log.level', - migration: true, - }, - { - name: 'message', - type: 'alias', - path: 'message', - migration: true, - }, - { - name: 'pid', - type: 'alias', - path: 'process.pid', - migration: true, - }, - { - name: 'tid', - type: 'alias', - path: 'process.thread.id', - migration: true, - }, - { - name: 'module', - type: 'alias', - path: 'apache.error.module', - migration: true, - }, - ], - }, - ], - }, - { - name: 'apache', - type: 'group', - description: 'Apache fields.\n', - fields: [ - { - name: 'access', - type: 'group', - description: 'Contains fields for the Apache HTTP Server access logs.\n', - fields: [ - { - name: 'ssl.protocol', - type: 'keyword', - description: 'SSL protocol version.\n', - }, - { - name: 'ssl.cipher', - type: 'keyword', - description: 'SSL cipher name.\n', - }, - ], - }, - { - name: 'error', - type: 'group', - description: 'Fields from the Apache error logs.\n', - fields: [ - { - name: 'module', - type: 'keyword', - description: 'The module producing the logged message.\n', - }, - ], - }, - ], - }, - ], - }, - { - key: 'auditd', - title: 'Auditd', - description: 'Module for parsing auditd logs.\n', - short_config: true, - fields: [ - { - name: 'user', - type: 'group', - fields: [ - { - name: 'terminal', - type: 'keyword', - description: - 'Terminal or tty device on which the user is performing the observed activity.\n', - }, - { - name: 'audit', - type: 'group', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'One or multiple unique identifiers of the user.\n', - }, - { - name: 'name', - type: 'keyword', - example: 'albert', - description: 'Short name or login of the user.\n', - }, - { - name: 'group.id', - type: 'keyword', - description: 'Unique identifier for the group on the system/platform.\n', - }, - { - name: 'group.name', - type: 'keyword', - description: 'Name of the group.\n', - }, - ], - }, - { - name: 'effective', - type: 'group', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'One or multiple unique identifiers of the user.\n', - }, - { - name: 'name', - type: 'keyword', - example: 'albert', - description: 'Short name or login of the user.\n', - }, - { - name: 'group.id', - type: 'keyword', - description: 'Unique identifier for the group on the system/platform.\n', - }, - { - name: 'group.name', - type: 'keyword', - description: 'Name of the group.\n', - }, - ], - }, - { - name: 'filesystem', - type: 'group', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'One or multiple unique identifiers of the user.\n', - }, - { - name: 'name', - type: 'keyword', - example: 'albert', - description: 'Short name or login of the user.\n', - }, - { - name: 'group.id', - type: 'keyword', - description: 'Unique identifier for the group on the system/platform.\n', - }, - { - name: 'group.name', - type: 'keyword', - description: 'Name of the group.\n', - }, - ], - }, - { - name: 'owner', - type: 'group', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'One or multiple unique identifiers of the user.\n', - }, - { - name: 'name', - type: 'keyword', - example: 'albert', - description: 'Short name or login of the user.\n', - }, - { - name: 'group.id', - type: 'keyword', - description: 'Unique identifier for the group on the system/platform.\n', - }, - { - name: 'group.name', - type: 'keyword', - description: 'Name of the group.\n', - }, - ], - }, - { - name: 'saved', - type: 'group', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'One or multiple unique identifiers of the user.\n', - }, - { - name: 'name', - type: 'keyword', - example: 'albert', - description: 'Short name or login of the user.\n', - }, - { - name: 'group.id', - type: 'keyword', - description: 'Unique identifier for the group on the system/platform.\n', - }, - { - name: 'group.name', - type: 'keyword', - description: 'Name of the group.\n', - }, - ], - }, - ], - }, - { - name: 'auditd', - type: 'group', - description: 'Fields from the auditd logs.\n', - fields: [ - { - name: 'log', - type: 'group', - description: - 'Fields from the Linux audit log. Not all fields are documented here because they are dynamic and vary by audit event type.\n', - fields: [ - { - name: 'old_auid', - description: - 'For login events this is the old audit ID used for the user prior to this login.\n', - }, - { - name: 'new_auid', - description: - 'For login events this is the new audit ID. The audit ID can be used to trace future events to the user even if their identity changes (like becoming root).\n', - }, - { - name: 'old_ses', - description: - 'For login events this is the old session ID used for the user prior to this login.\n', - }, - { - name: 'new_ses', - description: - 'For login events this is the new session ID. It can be used to tie a user to future events by session ID.\n', - }, - { - name: 'sequence', - type: 'long', - description: 'The audit event sequence number.\n', - }, - { - name: 'items', - description: 'The number of items in an event.\n', - }, - { - name: 'item', - description: - 'The item field indicates which item out of the total number of items. This number is zero-based; a value of 0 means it is the first item.\n', - }, - { - name: 'tty', - type: 'keyword', - definition: 'TTY udevice the user is running programs on.\n', - }, - { - name: 'a0', - description: 'The first argument to the system call.\n', - }, - { - name: 'addr', - type: 'ip', - definition: 'Remote address that the user is connecting from.\n', - }, - { - name: 'rport', - type: 'long', - definition: 'Remote port number.\n', - }, - { - name: 'laddr', - type: 'ip', - definition: 'Local network address.\n', - }, - { - name: 'lport', - type: 'long', - definition: 'Local port number.\n', - }, - { - name: 'acct', - type: 'alias', - path: 'user.name', - migration: true, - }, - { - name: 'pid', - type: 'alias', - path: 'process.pid', - migration: true, - }, - { - name: 'ppid', - type: 'alias', - path: 'process.ppid', - migration: true, - }, - { - name: 'res', - type: 'alias', - path: 'event.outcome', - migration: true, - }, - { - name: 'record_type', - type: 'alias', - path: 'event.action', - migration: true, - }, - { - name: 'geoip', - type: 'group', - fields: [ - { - name: 'continent_name', - type: 'alias', - path: 'source.geo.continent_name', - migration: true, - }, - { - name: 'country_iso_code', - type: 'alias', - path: 'source.geo.country_iso_code', - migration: true, - }, - { - name: 'location', - type: 'alias', - path: 'source.geo.location', - migration: true, - }, - { - name: 'region_name', - type: 'alias', - path: 'source.geo.region_name', - migration: true, - }, - { - name: 'city_name', - type: 'alias', - path: 'source.geo.city_name', - migration: true, - }, - { - name: 'region_iso_code', - type: 'alias', - path: 'source.geo.region_iso_code', - migration: true, - }, - ], - }, - { - name: 'arch', - type: 'alias', - path: 'host.architecture', - migration: true, - }, - { - name: 'gid', - type: 'alias', - path: 'user.group.id', - migration: true, - }, - { - name: 'uid', - type: 'alias', - path: 'user.id', - migration: true, - }, - { - name: 'agid', - type: 'alias', - path: 'user.audit.group.id', - migration: true, - }, - { - name: 'auid', - type: 'alias', - path: 'user.audit.id', - migration: true, - }, - { - name: 'fsgid', - type: 'alias', - path: 'user.filesystem.group.id', - migration: true, - }, - { - name: 'fsuid', - type: 'alias', - path: 'user.filesystem.id', - migration: true, - }, - { - name: 'egid', - type: 'alias', - path: 'user.effective.group.id', - migration: true, - }, - { - name: 'euid', - type: 'alias', - path: 'user.effective.id', - migration: true, - }, - { - name: 'sgid', - type: 'alias', - path: 'user.saved.group.id', - migration: true, - }, - { - name: 'suid', - type: 'alias', - path: 'user.saved.id', - migration: true, - }, - { - name: 'ogid', - type: 'alias', - path: 'user.owner.group.id', - migration: true, - }, - { - name: 'ouid', - type: 'alias', - path: 'user.owner.id', - migration: true, - }, - { - name: 'comm', - type: 'alias', - path: 'process.name', - migration: true, - }, - { - name: 'exe', - type: 'alias', - path: 'process.executable', - migration: true, - }, - { - name: 'terminal', - type: 'alias', - path: 'user.terminal', - migration: true, - }, - { - name: 'msg', - type: 'alias', - path: 'message', - migration: true, - }, - { - name: 'src', - type: 'alias', - path: 'source.address', - migration: true, - }, - { - name: 'dst', - type: 'alias', - path: 'destination.address', - migration: true, - }, - ], - }, - ], - }, - ], - }, - { - key: 'elasticsearch', - title: 'elasticsearch', - description: 'elasticsearch Module\n', - fields: [ - { - name: 'elasticsearch', - type: 'group', - description: '\n', - fields: [ - { - name: 'component', - description: 'Elasticsearch component from where the log event originated', - example: 'o.e.c.m.MetaDataCreateIndexService', - type: 'keyword', - }, - { - name: 'cluster.uuid', - description: 'UUID of the cluster', - example: 'GmvrbHlNTiSVYiPf8kxg9g', - type: 'keyword', - }, - { - name: 'cluster.name', - description: 'Name of the cluster', - example: 'docker-cluster', - type: 'keyword', - }, - { - name: 'node.id', - description: 'ID of the node', - example: 'DSiWcTyeThWtUXLB9J0BMw', - type: 'keyword', - }, - { - name: 'node.name', - description: 'Name of the node', - example: 'vWNJsZ3', - type: 'keyword', - }, - { - name: 'index.name', - description: 'Index name', - example: 'filebeat-test-input', - type: 'keyword', - }, - { - name: 'index.id', - description: 'Index id', - example: 'aOGgDwbURfCV57AScqbCgw', - type: 'keyword', - }, - { - name: 'shard.id', - description: 'Id of the shard', - example: '0', - type: 'keyword', - }, - { - name: 'audit', - type: 'group', - description: '\n', - fields: [ - { - name: 'layer', - description: - 'The layer from which this event originated: rest, transport or ip_filter', - example: 'rest', - type: 'keyword', - }, - { - name: 'event_type', - description: - 'The type of event that occurred: anonymous_access_denied, authentication_failed, access_denied, access_granted, connection_granted, connection_denied, tampered_request, run_as_granted, run_as_denied', - example: 'access_granted', - type: 'keyword', - }, - { - name: 'origin.type', - description: - 'Where the request originated: rest (request originated from a REST API request), transport (request was received on the transport channel), local_node (the local node issued the request)', - example: 'local_node', - type: 'keyword', - }, - { - name: 'realm', - description: 'The authentication realm the authentication was validated against', - example: 'default_file', - type: 'keyword', - }, - { - name: 'user.realm', - description: "The user's authentication realm, if authenticated", - example: 'active_directory', - type: 'keyword', - }, - { - name: 'user.roles', - description: 'Roles to which the principal belongs', - example: ['kibana_user', 'beats_admin'], - type: 'keyword', - }, - { - name: 'action', - description: 'The name of the action that was executed', - example: 'cluster:monitor/main', - type: 'keyword', - }, - { - name: 'url.params', - description: 'REST URI parameters', - example: '{username=jacknich2}', - }, - { - name: 'indices', - description: 'Indices accessed by action', - example: ['foo-2019.01.04', 'foo-2019.01.03', 'foo-2019.01.06'], - type: 'keyword', - }, - { - name: 'request.id', - description: 'Unique ID of request', - example: 'WzL_kb6VSvOhAq0twPvHOQ', - type: 'keyword', - }, - { - name: 'request.name', - description: 'The type of request that was executed', - example: 'ClearScrollRequest', - type: 'keyword', - }, - { - name: 'request_body', - type: 'alias', - path: 'http.request.body.content', - migration: true, - }, - { - name: 'origin_address', - type: 'alias', - path: 'source.ip', - migration: true, - }, - { - name: 'uri', - type: 'alias', - path: 'url.original', - migration: true, - }, - { - name: 'principal', - type: 'alias', - path: 'user.name', - migration: true, - }, - { - name: 'message', - type: 'text', - }, - ], - }, - { - name: 'gc', - type: 'group', - description: 'GC fileset fields.\n', - fields: [ - { - name: 'phase', - type: 'group', - description: 'Fields specific to GC phase.\n', - fields: [ - { - name: 'name', - type: 'keyword', - description: 'Name of the GC collection phase.\n', - }, - { - name: 'duration_sec', - type: 'float', - description: - 'Collection phase duration according to the Java virtual machine.\n', - }, - { - name: 'scrub_symbol_table_time_sec', - type: 'float', - description: 'Pause time in seconds cleaning up symbol tables.\n', - }, - { - name: 'scrub_string_table_time_sec', - type: 'float', - description: 'Pause time in seconds cleaning up string tables.\n', - }, - { - name: 'weak_refs_processing_time_sec', - type: 'float', - description: 'Time spent processing weak references in seconds.\n', - }, - { - name: 'parallel_rescan_time_sec', - type: 'float', - description: - 'Time spent in seconds marking live objects while application is stopped.\n', - }, - { - name: 'class_unload_time_sec', - type: 'float', - description: 'Time spent unloading unused classes in seconds.\n', - }, - { - name: 'cpu_time', - type: 'group', - description: 'Process CPU time spent performing collections.\n', - fields: [ - { - name: 'user_sec', - type: 'float', - description: 'CPU time spent outside the kernel.\n', - }, - { - name: 'sys_sec', - type: 'float', - description: 'CPU time spent inside the kernel.\n', - }, - { - name: 'real_sec', - type: 'float', - description: - 'Total elapsed CPU time spent to complete the collection from start to finish.\n', - }, - ], - }, - ], - }, - { - name: 'jvm_runtime_sec', - type: 'float', - description: 'The time from JVM start up in seconds, as a floating point number.\n', - }, - { - name: 'threads_total_stop_time_sec', - type: 'float', - description: 'Garbage collection threads total stop time seconds.\n', - }, - { - name: 'stopping_threads_time_sec', - type: 'float', - description: 'Time took to stop threads seconds.\n', - }, - { - name: 'tags', - type: 'keyword', - description: 'GC logging tags.\n', - }, - { - name: 'heap', - type: 'group', - description: 'Heap allocation and total size.\n', - fields: [ - { - name: 'size_kb', - type: 'integer', - description: 'Total heap size in kilobytes.\n', - }, - { - name: 'used_kb', - type: 'integer', - description: 'Used heap in kilobytes.\n', - }, - ], - }, - { - name: 'old_gen', - type: 'group', - description: 'Old generation occupancy and total size.\n', - fields: [ - { - name: 'size_kb', - type: 'integer', - description: 'Total size of old generation in kilobytes.\n', - }, - { - name: 'used_kb', - type: 'integer', - description: 'Old generation occupancy in kilobytes.\n', - }, - ], - }, - { - name: 'young_gen', - type: 'group', - description: 'Young generation occupancy and total size.\n', - fields: [ - { - name: 'size_kb', - type: 'integer', - description: 'Total size of young generation in kilobytes.\n', - }, - { - name: 'used_kb', - type: 'integer', - description: 'Young generation occupancy in kilobytes.\n', - }, - ], - }, - ], - }, - { - name: 'server', - description: 'Server log file', - type: 'group', - fields: [ - { - name: 'stacktrace', - description: 'Stack trace in case of errors', - index: false, - }, - { - name: 'gc', - description: 'GC log', - type: 'group', - fields: [ - { - name: 'young', - description: 'Young GC', - example: '', - type: 'group', - fields: [ - { - name: 'one', - description: '', - example: '', - type: 'long', - }, - { - name: 'two', - description: '', - example: '', - type: 'long', - }, - ], - }, - { - name: 'overhead_seq', - description: 'Sequence number', - example: 3449992, - type: 'long', - }, - { - name: 'collection_duration.ms', - description: 'Time spent in GC, in milliseconds', - example: 1600, - type: 'float', - }, - { - name: 'observation_duration.ms', - description: 'Total time over which collection was observed, in milliseconds', - example: 1800, - type: 'float', - }, - ], - }, - ], - }, - { - name: 'slowlog', - description: 'Slowlog events from Elasticsearch', - example: - '[2018-06-29T10:06:14,933][INFO ][index.search.slowlog.query] [v_VJhjV] [metricbeat-6.3.0-2018.06.26][0] took[4.5ms], took_millis[4], total_hits[19435], types[], stats[], search_type[QUERY_THEN_FETCH], total_shards[1], source[{"query":{"match_all":{"boost":1.0}}}],', - type: 'group', - fields: [ - { - name: 'logger', - description: 'Logger name', - example: 'index.search.slowlog.fetch', - type: 'keyword', - }, - { - name: 'took', - description: 'Time it took to execute the query', - example: '300ms', - type: 'keyword', - }, - { - name: 'types', - description: 'Types', - example: '', - type: 'keyword', - }, - { - name: 'stats', - description: 'Stats groups', - example: 'group1', - type: 'keyword', - }, - { - name: 'search_type', - description: 'Search type', - example: 'QUERY_THEN_FETCH', - type: 'keyword', - }, - { - name: 'source_query', - description: 'Slow query', - example: '{"query":{"match_all":{"boost":1.0}}}', - type: 'keyword', - }, - { - name: 'extra_source', - description: 'Extra source information', - example: '', - type: 'keyword', - }, - { - name: 'total_hits', - description: 'Total hits', - example: 42, - type: 'keyword', - }, - { - name: 'total_shards', - description: 'Total queried shards', - example: 22, - type: 'keyword', - }, - { - name: 'routing', - description: 'Routing', - example: 's01HZ2QBk9jw4gtgaFtn', - type: 'keyword', - }, - { - name: 'id', - description: 'Id', - example: '', - type: 'keyword', - }, - { - name: 'type', - description: 'Type', - example: 'doc', - type: 'keyword', - }, - { - name: 'source', - description: 'Source of document that was indexed', - type: 'keyword', - }, - ], - }, - ], - }, - ], - }, - { - key: 'haproxy', - title: 'haproxy', - description: 'haproxy Module\n', - fields: [ - { - name: 'haproxy', - type: 'group', - description: '\n', - fields: [ - { - name: 'frontend_name', - description: - 'Name of the frontend (or listener) which received and processed the connection.', - }, - { - name: 'backend_name', - description: - 'Name of the backend (or listener) which was selected to manage the connection to the server.', - }, - { - name: 'server_name', - description: 'Name of the last server to which the connection was sent.', - }, - { - name: 'total_waiting_time_ms', - description: 'Total time in milliseconds spent waiting in the various queues', - type: 'long', - }, - { - name: 'connection_wait_time_ms', - description: - 'Total time in milliseconds spent waiting for the connection to establish to the final server', - type: 'long', - }, - { - name: 'bytes_read', - description: 'Total number of bytes transmitted to the client when the log is emitted.', - type: 'long', - }, - { - name: 'time_queue', - description: 'Total time in milliseconds spent waiting in the various queues.', - type: 'long', - }, - { - name: 'time_backend_connect', - description: - 'Total time in milliseconds spent waiting for the connection to establish to the final server, including retries.', - type: 'long', - }, - { - name: 'server_queue', - description: - 'Total number of requests which were processed before this one in the server queue.', - type: 'long', - }, - { - name: 'backend_queue', - description: - "Total number of requests which were processed before this one in the backend's global queue.", - type: 'long', - }, - { - name: 'bind_name', - description: 'Name of the listening address which received the connection.', - }, - { - name: 'error_message', - description: 'Error message logged by HAProxy in case of error.', - type: 'text', - }, - { - name: 'source', - type: 'keyword', - description: 'The HAProxy source of the log', - }, - { - name: 'termination_state', - description: 'Condition the session was in when the session ended.', - }, - { - name: 'mode', - type: 'keyword', - description: 'mode that the frontend is operating (TCP or HTTP)', - }, - { - name: 'connections', - description: 'Contains various counts of connections active in the process.', - type: 'group', - fields: [ - { - name: 'active', - description: - 'Total number of concurrent connections on the process when the session was logged.', - type: 'long', - }, - { - name: 'frontend', - description: - 'Total number of concurrent connections on the frontend when the session was logged.', - type: 'long', - }, - { - name: 'backend', - description: - 'Total number of concurrent connections handled by the backend when the session was logged.', - type: 'long', - }, - { - name: 'server', - description: - 'Total number of concurrent connections still active on the server when the session was logged.', - type: 'long', - }, - { - name: 'retries', - description: - 'Number of connection retries experienced by this session when trying to connect to the server.', - type: 'long', - }, - ], - }, - { - name: 'client', - description: 'Information about the client doing the request', - type: 'group', - fields: [ - { - name: 'ip', - type: 'alias', - path: 'source.address', - migration: true, - }, - { - name: 'port', - type: 'alias', - path: 'source.port', - migration: true, - }, - ], - }, - { - name: 'process_name', - type: 'alias', - path: 'process.name', - migration: true, - }, - { - name: 'pid', - type: 'alias', - path: 'process.pid', - migration: true, - }, - { - name: 'destination', - description: 'Destination information', - type: 'group', - fields: [ - { - name: 'port', - type: 'alias', - path: 'destination.port', - migration: true, - }, - { - name: 'ip', - type: 'alias', - path: 'destination.ip', - migration: true, - }, - ], - }, - { - name: 'geoip', - type: 'group', - description: - 'Contains GeoIP information gathered based on the client.ip field. Only present if the GeoIP Elasticsearch plugin is available and used.\n', - fields: [ - { - name: 'continent_name', - type: 'alias', - path: 'source.geo.continent_name', - migration: true, - }, - { - name: 'country_iso_code', - type: 'alias', - path: 'source.geo.country_iso_code', - migration: true, - }, - { - name: 'location', - type: 'alias', - path: 'source.geo.location', - migration: true, - }, - { - name: 'region_name', - type: 'alias', - path: 'source.geo.region_name', - migration: true, - }, - { - name: 'city_name', - type: 'alias', - path: 'source.geo.city_name', - migration: true, - }, - { - name: 'region_iso_code', - type: 'alias', - path: 'source.geo.region_iso_code', - migration: true, - }, - ], - }, - { - name: 'http', - description: 'Please add description', - type: 'group', - fields: [ - { - name: 'response', - description: 'Fields related to the HTTP response', - type: 'group', - fields: [ - { - name: 'captured_cookie', - description: - 'Optional "name=value" entry indicating that the client had this cookie in the response.\n', - }, - { - name: 'captured_headers', - description: - 'List of headers captured in the response due to the presence of the "capture response header" statement in the frontend.\n', - type: 'keyword', - }, - { - name: 'status_code', - type: 'alias', - path: 'http.response.status_code', - migration: true, - }, - ], - }, - { - name: 'request', - description: 'Fields related to the HTTP request', - type: 'group', - fields: [ - { - name: 'captured_cookie', - description: - 'Optional "name=value" entry indicating that the server has returned a cookie with its request.\n', - }, - { - name: 'captured_headers', - description: - 'List of headers captured in the request due to the presence of the "capture request header" statement in the frontend.\n', - type: 'keyword', - }, - { - name: 'raw_request_line', - description: - 'Complete HTTP request line, including the method, request and HTTP version string.', - type: 'keyword', - }, - { - name: 'time_wait_without_data_ms', - description: - 'Total time in milliseconds spent waiting for the server to send a full HTTP response, not counting data.', - type: 'long', - }, - { - name: 'time_wait_ms', - description: - 'Total time in milliseconds spent waiting for a full HTTP request from the client (not counting body) after the first byte was received.', - type: 'long', - }, - ], - }, - ], - }, - { - name: 'tcp', - description: 'TCP log format', - type: 'group', - fields: [ - { - name: 'connection_waiting_time_ms', - type: 'long', - description: - 'Total time in milliseconds elapsed between the accept and the last close', - }, - ], - }, - ], - }, - ], - }, - { - key: 'icinga', - title: 'Icinga', - description: 'Icinga Module\n', - fields: [ - { - name: 'icinga', - type: 'group', - description: '\n', - fields: [ - { - name: 'debug', - type: 'group', - description: 'Contains fields for the Icinga debug logs.\n', - fields: [ - { - name: 'facility', - type: 'keyword', - description: 'Specifies what component of Icinga logged the message.\n', - }, - { - name: 'severity', - type: 'alias', - path: 'log.level', - migration: true, - }, - { - name: 'message', - type: 'alias', - path: 'message', - migration: true, - }, - ], - }, - { - name: 'main', - type: 'group', - description: 'Contains fields for the Icinga main logs.\n', - fields: [ - { - name: 'facility', - type: 'keyword', - description: 'Specifies what component of Icinga logged the message.\n', - }, - { - name: 'severity', - type: 'alias', - path: 'log.level', - migration: true, - }, - { - name: 'message', - type: 'alias', - path: 'message', - migration: true, - }, - ], - }, - { - name: 'startup', - type: 'group', - description: 'Contains fields for the Icinga startup logs.\n', - fields: [ - { - name: 'facility', - type: 'keyword', - description: 'Specifies what component of Icinga logged the message.\n', - }, - { - name: 'severity', - type: 'alias', - path: 'log.level', - migration: true, - }, - { - name: 'message', - type: 'alias', - path: 'message', - migration: true, - }, - ], - }, - ], - }, - ], - }, - { - key: 'iis', - title: 'IIS', - description: 'Module for parsing IIS log files.\n', - fields: [ - { - name: 'iis', - type: 'group', - description: 'Fields from IIS log files.\n', - fields: [ - { - name: 'access', - type: 'group', - description: 'Contains fields for IIS access logs.\n', - fields: [ - { - name: 'sub_status', - type: 'long', - description: 'The HTTP substatus code.\n', - }, - { - name: 'win32_status', - type: 'long', - description: 'The Windows status code.\n', - }, - { - name: 'site_name', - type: 'keyword', - description: 'The site name and instance number.\n', - }, - { - name: 'server_name', - type: 'keyword', - description: 'The name of the server on which the log file entry was generated.\n', - }, - { - name: 'cookie', - type: 'keyword', - description: 'The content of the cookie sent or received, if any.\n', - }, - { - name: 'body_received.bytes', - type: 'alias', - path: 'http.request.body.bytes', - migration: true, - }, - { - name: 'body_sent.bytes', - type: 'alias', - path: 'http.response.body.bytes', - migration: true, - }, - { - name: 'server_ip', - type: 'alias', - path: 'destination.address', - migration: true, - }, - { - name: 'method', - type: 'alias', - path: 'http.request.method', - migration: true, - }, - { - name: 'url', - type: 'alias', - path: 'url.path', - migration: true, - }, - { - name: 'query_string', - type: 'alias', - path: 'url.query', - migration: true, - }, - { - name: 'port', - type: 'alias', - path: 'destination.port', - migration: true, - }, - { - name: 'user_name', - type: 'alias', - path: 'user.name', - migration: true, - }, - { - name: 'remote_ip', - type: 'alias', - path: 'source.address', - migration: true, - }, - { - name: 'referrer', - type: 'alias', - path: 'http.request.referrer', - migration: true, - }, - { - name: 'response_code', - type: 'alias', - path: 'http.response.status_code', - migration: true, - }, - { - name: 'http_version', - type: 'alias', - path: 'http.version', - migration: true, - }, - { - name: 'hostname', - type: 'alias', - path: 'host.hostname', - migration: true, - }, - { - name: 'user_agent', - type: 'group', - fields: [ - { - name: 'device', - type: 'alias', - path: 'user_agent.device.name', - migration: true, - }, - { - name: 'name', - type: 'alias', - path: 'user_agent.name', - migration: true, - }, - { - name: 'os', - type: 'alias', - path: 'user_agent.os.full_name', - migration: true, - }, - { - name: 'os_name', - type: 'alias', - path: 'user_agent.os.name', - migration: true, - }, - { - name: 'original', - type: 'alias', - path: 'user_agent.original', - migration: true, - }, - ], - }, - { - name: 'geoip', - type: 'group', - fields: [ - { - name: 'continent_name', - type: 'alias', - path: 'source.geo.continent_name', - migration: true, - }, - { - name: 'country_iso_code', - type: 'alias', - path: 'source.geo.country_iso_code', - migration: true, - }, - { - name: 'location', - type: 'alias', - path: 'source.geo.location', - migration: true, - }, - { - name: 'region_name', - type: 'alias', - path: 'source.geo.region_name', - migration: true, - }, - { - name: 'city_name', - type: 'alias', - path: 'source.geo.city_name', - migration: true, - }, - { - name: 'region_iso_code', - type: 'alias', - path: 'source.geo.region_iso_code', - migration: true, - }, - ], - }, - ], - }, - { - name: 'error', - type: 'group', - description: 'Contains fields for IIS error logs.\n', - fields: [ - { - name: 'reason_phrase', - type: 'keyword', - description: 'The HTTP reason phrase.\n', - }, - { - name: 'queue_name', - type: 'keyword', - description: 'The IIS application pool name.\n', - }, - { - name: 'remote_ip', - type: 'alias', - path: 'source.address', - migration: true, - }, - { - name: 'remote_port', - type: 'alias', - path: 'source.port', - migration: true, - }, - { - name: 'server_ip', - type: 'alias', - path: 'destination.address', - migration: true, - }, - { - name: 'server_port', - type: 'alias', - path: 'destination.port', - migration: true, - }, - { - name: 'http_version', - type: 'alias', - path: 'http.version', - migration: true, - }, - { - name: 'method', - type: 'alias', - path: 'http.request.method', - migration: true, - }, - { - name: 'url', - type: 'alias', - path: 'url.original', - migration: true, - }, - { - name: 'response_code', - type: 'alias', - path: 'http.response.status_code', - migration: true, - }, - { - name: 'geoip', - type: 'group', - fields: [ - { - name: 'continent_name', - type: 'alias', - path: 'source.geo.continent_name', - migration: true, - }, - { - name: 'country_iso_code', - type: 'alias', - path: 'source.geo.country_iso_code', - migration: true, - }, - { - name: 'location', - type: 'alias', - path: 'source.geo.location', - migration: true, - }, - { - name: 'region_name', - type: 'alias', - path: 'source.geo.region_name', - migration: true, - }, - { - name: 'city_name', - type: 'alias', - path: 'source.geo.city_name', - migration: true, - }, - { - name: 'region_iso_code', - type: 'alias', - path: 'source.geo.region_iso_code', - migration: true, - }, - ], - }, - ], - }, - ], - }, - ], - }, - { - key: 'kafka', - title: 'Kafka', - description: 'Kafka module\n', - fields: [ - { - name: 'kafka', - type: 'group', - description: '\n', - fields: [ - { - name: 'log', - type: 'group', - description: 'Kafka log lines.\n', - fields: [ - { - name: 'level', - type: 'alias', - path: 'log.level', - migration: true, - }, - { - name: 'message', - type: 'alias', - path: 'message', - migration: true, - }, - { - name: 'component', - type: 'keyword', - description: 'Component the log is coming from.\n', - }, - { - name: 'class', - type: 'keyword', - description: 'Java class the log is coming from.\n', - }, - { - name: 'trace', - type: 'group', - description: 'Trace in the log line.\n', - fields: [ - { - name: 'class', - type: 'keyword', - description: 'Java class the trace is coming from.\n', - }, - { - name: 'message', - type: 'text', - description: 'Message part of the trace.\n', - }, - ], - }, - ], - }, - ], - }, - ], - }, - { - key: 'kibana', - title: 'kibana', - description: 'kibana Module\n', - fields: [ - { - name: 'kibana', - type: 'group', - description: '\n', - fields: [ - { - name: 'log', - type: 'group', - description: 'Kafka log lines.\n', - fields: [ - { - name: 'tags', - type: 'keyword', - description: 'Kibana logging tags.\n', - }, - { - name: 'state', - type: 'keyword', - description: 'Current state of Kibana.\n', - }, - { - name: 'meta', - type: 'object', - object_type: 'keyword', - }, - { - name: 'kibana.log.meta.req.headers.referer', - type: 'alias', - path: 'http.request.referrer', - migration: true, - }, - { - name: 'kibana.log.meta.req.referer', - type: 'alias', - path: 'http.request.referrer', - migration: true, - }, - { - name: 'kibana.log.meta.req.headers.user-agent', - type: 'alias', - path: 'user_agent.original', - migration: true, - }, - { - name: 'kibana.log.meta.req.remoteAddress', - type: 'alias', - path: 'source.address', - migration: true, - }, - { - name: 'kibana.log.meta.req.url', - type: 'alias', - path: 'url.original', - migration: true, - }, - { - name: 'kibana.log.meta.statusCode', - type: 'alias', - path: 'http.response.status_code', - migration: true, - }, - { - name: 'kibana.log.meta.method', - type: 'alias', - path: 'http.request.method', - migration: true, - }, - ], - }, - ], - }, - ], - }, - { - key: 'logstash', - title: 'logstash', - description: 'logstash Module\n', - fields: [ - { - name: 'logstash', - type: 'group', - description: '\n', - fields: [ - { - name: 'log', - title: 'Logstash', - type: 'group', - description: 'Fields from the Logstash logs.\n', - fields: [ - { - name: 'module', - type: 'keyword', - description: 'The module or class where the event originate.\n', - }, - { - name: 'thread', - type: 'keyword', - description: 'Information about the running thread where the log originate.\n', - multi_fields: [ - { - name: 'text', - type: 'text', - }, - ], - }, - { - name: 'log_event', - type: 'object', - description: 'key and value debugging information.\n', - }, - { - name: 'pipeline_id', - type: 'keyword', - example: 'main', - description: 'The ID of the pipeline.\n', - }, - { - name: 'message', - type: 'alias', - path: 'message', - migration: true, - }, - { - name: 'level', - type: 'alias', - path: 'log.level', - migration: true, - }, - ], - }, - { - name: 'slowlog', - type: 'group', - description: 'slowlog\n', - fields: [ - { - name: 'module', - type: 'keyword', - description: 'The module or class where the event originate.\n', - }, - { - name: 'thread', - type: 'keyword', - description: 'Information about the running thread where the log originate.\n', - multi_fields: [ - { - name: 'text', - type: 'text', - }, - ], - }, - { - name: 'event', - type: 'keyword', - description: 'Raw dump of the original event\n', - multi_fields: [ - { - name: 'text', - type: 'text', - }, - ], - }, - { - name: 'plugin_name', - type: 'keyword', - description: 'Name of the plugin\n', - }, - { - name: 'plugin_type', - type: 'keyword', - description: 'Type of the plugin: Inputs, Filters, Outputs or Codecs.\n', - }, - { - name: 'took_in_millis', - type: 'long', - description: 'Execution time for the plugin in milliseconds.\n', - }, - { - name: 'plugin_params', - type: 'keyword', - description: 'String value of the plugin configuration\n', - multi_fields: [ - { - name: 'text', - type: 'text', - }, - ], - }, - { - name: 'plugin_params_object', - type: 'object', - description: 'key -> value of the configuration used by the plugin.\n', - }, - { - name: 'level', - type: 'alias', - path: 'log.level', - migration: true, - }, - { - name: 'took_in_nanos', - type: 'alias', - path: 'event.duration', - migration: true, - }, - ], - }, - ], - }, - ], - }, - { - key: 'mongodb', - title: 'mongodb', - description: 'Module for parsing MongoDB log files.\n', - fields: [ - { - name: 'mongodb', - type: 'group', - description: 'Fields from MongoDB logs.\n', - fields: [ - { - name: 'log', - type: 'group', - description: 'Contains fields from MongoDB logs.\n', - fields: [ - { - name: 'component', - description: 'Functional categorization of message\n', - example: 'COMMAND', - type: 'keyword', - }, - { - name: 'context', - description: 'Context of message\n', - example: 'initandlisten', - type: 'keyword', - }, - { - name: 'severity', - type: 'alias', - path: 'log.level', - migration: true, - }, - { - name: 'message', - type: 'alias', - path: 'message', - migration: true, - }, - ], - }, - ], - }, - ], - }, - { - key: 'mysql', - title: 'MySQL', - description: 'Module for parsing the MySQL log files.\n', - short_config: true, - fields: [ - { - name: 'mysql', - type: 'group', - description: 'Fields from the MySQL log files.\n', - fields: [ - { - name: 'thread_id', - type: 'long', - description: 'The connection or thread ID for the query.\n', - }, - { - name: 'error', - type: 'group', - description: 'Contains fields from the MySQL error logs.\n', - fields: [ - { - name: 'thread_id', - type: 'alias', - path: 'mysql.thread_id', - migration: true, - }, - { - name: 'level', - type: 'alias', - path: 'log.level', - migration: true, - }, - { - name: 'message', - type: 'alias', - path: 'message', - migration: true, - }, - ], - }, - { - name: 'slowlog', - type: 'group', - description: 'Contains fields from the MySQL slow logs.\n', - fields: [ - { - name: 'lock_time.sec', - type: 'float', - description: - 'The amount of time the query waited for the lock to be available. The value is in seconds, as a floating point number.\n', - }, - { - name: 'rows_sent', - type: 'long', - description: 'The number of rows returned by the query.\n', - }, - { - name: 'rows_examined', - type: 'long', - description: 'The number of rows scanned by the query.\n', - }, - { - name: 'rows_affected', - type: 'long', - description: 'The number of rows modified by the query.\n', - }, - { - name: 'bytes_sent', - type: 'long', - format: 'bytes', - description: 'The number of bytes sent to client.\n', - }, - { - name: 'bytes_received', - type: 'long', - format: 'bytes', - description: 'The number of bytes received from client.\n', - }, - { - name: 'query', - description: 'The slow query.\n', - }, - { - name: 'id', - type: 'alias', - path: 'mysql.thread_id', - migration: true, - }, - { - name: 'schema', - type: 'keyword', - description: 'The schema where the slow query was executed.\n', - }, - { - name: 'current_user', - type: 'keyword', - description: - 'Current authenticated user, used to determine access privileges. Can differ from the value for user.\n', - }, - { - name: 'last_errno', - type: 'keyword', - description: 'Last SQL error seen.\n', - }, - { - name: 'killed', - type: 'keyword', - description: 'Code of the reason if the query was killed.\n', - }, - { - name: 'query_cache_hit', - type: 'boolean', - description: 'Whether the query cache was hit.\n', - }, - { - name: 'tmp_table', - type: 'boolean', - description: 'Whether a temporary table was used to resolve the query.\n', - }, - { - name: 'tmp_table_on_disk', - type: 'boolean', - description: 'Whether the query needed temporary tables on disk.\n', - }, - { - name: 'tmp_tables', - type: 'long', - description: 'Number of temporary tables created for this query\n', - }, - { - name: 'tmp_disk_tables', - type: 'long', - description: 'Number of temporary tables created on disk for this query.\n', - }, - { - name: 'tmp_table_sizes', - type: 'long', - format: 'bytes', - description: 'Size of temporary tables created for this query.', - }, - { - name: 'filesort', - type: 'boolean', - description: 'Whether filesort optimization was used.\n', - }, - { - name: 'filesort_on_disk', - type: 'boolean', - description: - 'Whether filesort optimization was used and it needed temporary tables on disk.\n', - }, - { - name: 'priority_queue', - type: 'boolean', - description: 'Whether a priority queue was used for filesort.\n', - }, - { - name: 'full_scan', - type: 'boolean', - description: 'Whether a full table scan was needed for the slow query.\n', - }, - { - name: 'full_join', - type: 'boolean', - description: - 'Whether a full join was needed for the slow query (no indexes were used for joins).\n', - }, - { - name: 'merge_passes', - type: 'long', - description: 'Number of merge passes executed for the query.\n', - }, - { - name: 'sort_merge_passes', - type: 'long', - description: 'Number of merge passes that the sort algorithm has had to do.\n', - }, - { - name: 'sort_range_count', - type: 'long', - description: 'Number of sorts that were done using ranges.\n', - }, - { - name: 'sort_rows', - type: 'long', - description: 'Number of sorted rows.\n', - }, - { - name: 'sort_scan_count', - type: 'long', - description: 'Number of sorts that were done by scanning the table.\n', - }, - { - name: 'log_slow_rate_type', - type: 'keyword', - description: - 'Type of slow log rate limit, it can be `session` if the rate limit is applied per session, or `query` if it applies per query.\n', - }, - { - name: 'log_slow_rate_limit', - type: 'keyword', - description: - 'Slow log rate limit, a value of 100 means that one in a hundred queries or sessions are being logged.\n', - }, - { - name: 'read_first', - type: 'long', - description: 'The number of times the first entry in an index was read.\n', - }, - { - name: 'read_last', - type: 'long', - description: 'The number of times the last key in an index was read.\n', - }, - { - name: 'read_key', - type: 'long', - description: 'The number of requests to read a row based on a key.\n', - }, - { - name: 'read_next', - type: 'long', - description: 'The number of requests to read the next row in key order.\n', - }, - { - name: 'read_prev', - type: 'long', - description: 'The number of requests to read the previous row in key order.\n', - }, - { - name: 'read_rnd', - type: 'long', - description: 'The number of requests to read a row based on a fixed position.\n', - }, - { - name: 'read_rnd_next', - type: 'long', - description: 'The number of requests to read the next row in the data file.\n', - }, - { - name: 'innodb', - type: 'group', - description: 'Contains fields relative to InnoDB engine\n', - fields: [ - { - name: 'trx_id', - type: 'keyword', - description: 'Transaction ID\n', - }, - { - name: 'io_r_ops', - type: 'long', - description: 'Number of page read operations.\n', - }, - { - name: 'io_r_bytes', - type: 'long', - format: 'bytes', - description: 'Bytes read during page read operations.\n', - }, - { - name: 'io_r_wait.sec', - type: 'long', - description: 'How long it took to read all needed data from storage.\n', - }, - { - name: 'rec_lock_wait.sec', - type: 'long', - description: 'How long the query waited for locks.\n', - }, - { - name: 'queue_wait.sec', - type: 'long', - description: - 'How long the query waited to enter the InnoDB queue and to be executed once in the queue.\n', - }, - { - name: 'pages_distinct', - type: 'long', - description: 'Approximated count of pages accessed to execute the query.\n', - }, - ], - }, - { - name: 'user', - type: 'alias', - path: 'user.name', - migration: true, - }, - { - name: 'host', - type: 'alias', - path: 'source.domain', - migration: true, - }, - { - name: 'ip', - type: 'alias', - path: 'source.ip', - migration: true, - }, - ], - }, - ], - }, - ], - }, - { - key: 'nats', - title: 'nats', - description: 'Module for parsing NATS log files.\n', - release: 'beta', - fields: [ - { - name: 'nats', - type: 'group', - description: 'Fields from NATS logs.\n', - fields: [ - { - name: 'log', - type: 'group', - description: 'Nats log files\n', - release: 'beta', - fields: [ - { - name: 'client', - type: 'group', - description: 'Fields from NATS logs client.\n', - fields: [ - { - name: 'id', - type: 'integer', - description: 'The id of the client\n', - }, - ], - }, - { - name: 'msg', - type: 'group', - description: 'Fields from NATS logs message.\n', - fields: [ - { - name: 'bytes', - type: 'long', - format: 'bytes', - description: 'Size of the payload in bytes\n', - }, - { - name: 'type', - type: 'keyword', - description: 'The protocol message type\n', - }, - { - name: 'subject', - type: 'keyword', - description: 'Subject name this message was received on\n', - }, - { - name: 'sid', - type: 'integer', - description: 'The unique alphanumeric subscription ID of the subject\n', - }, - { - name: 'reply_to', - type: 'keyword', - description: - 'The inbox subject on which the publisher is listening for responses\n', - }, - { - name: 'max_messages', - type: 'integer', - description: - 'An optional number of messages to wait for before automatically unsubscribing\n', - }, - { - name: 'error.message', - type: 'text', - description: 'Details about the error occurred\n', - }, - { - name: 'queue_group', - type: 'text', - description: 'The queue group which subscriber will join\n', - }, - ], - }, - ], - }, - ], - }, - ], - }, - { - key: 'nginx', - title: 'Nginx', - description: 'Module for parsing the Nginx log files.\n', - short_config: true, - fields: [ - { - name: 'nginx', - type: 'group', - description: 'Fields from the Nginx log files.\n', - fields: [ - { - name: 'access', - type: 'group', - description: 'Contains fields for the Nginx access logs.\n', - fields: [ - { - name: 'remote_ip_list', - type: 'array', - description: - 'An array of remote IP addresses. It is a list because it is common to include, besides the client IP address, IP addresses from headers like `X-Forwarded-For`. Real source IP is restored to `source.ip`.\n', - }, - { - name: 'body_sent.bytes', - type: 'alias', - path: 'http.response.body.bytes', - migration: true, - }, - { - name: 'user_name', - type: 'alias', - path: 'user.name', - migration: true, - }, - { - name: 'method', - type: 'alias', - path: 'http.request.method', - migration: true, - }, - { - name: 'url', - type: 'alias', - path: 'url.original', - migration: true, - }, - { - name: 'http_version', - type: 'alias', - path: 'http.version', - migration: true, - }, - { - name: 'response_code', - type: 'alias', - path: 'http.response.status_code', - migration: true, - }, - { - name: 'referrer', - type: 'alias', - path: 'http.request.referrer', - migration: true, - }, - { - name: 'agent', - type: 'alias', - path: 'user_agent.original', - migration: true, - }, - { - name: 'user_agent', - type: 'group', - fields: [ - { - name: 'device', - type: 'alias', - path: 'user_agent.device.name', - migration: true, - }, - { - name: 'name', - type: 'alias', - path: 'user_agent.name', - migration: true, - }, - { - name: 'os', - type: 'alias', - path: 'user_agent.os.full_name', - migration: true, - }, - { - name: 'os_name', - type: 'alias', - path: 'user_agent.os.name', - migration: true, - }, - { - name: 'original', - type: 'alias', - path: 'user_agent.original', - migration: true, - }, - ], - }, - { - name: 'geoip', - type: 'group', - fields: [ - { - name: 'continent_name', - type: 'alias', - path: 'source.geo.continent_name', - migration: true, - }, - { - name: 'country_iso_code', - type: 'alias', - path: 'source.geo.country_iso_code', - migration: true, - }, - { - name: 'location', - type: 'alias', - path: 'source.geo.location', - migration: true, - }, - { - name: 'region_name', - type: 'alias', - path: 'source.geo.region_name', - migration: true, - }, - { - name: 'city_name', - type: 'alias', - path: 'source.geo.city_name', - migration: true, - }, - { - name: 'region_iso_code', - type: 'alias', - path: 'source.geo.region_iso_code', - migration: true, - }, - ], - }, - ], - }, - { - name: 'error', - type: 'group', - description: 'Contains fields for the Nginx error logs.\n', - fields: [ - { - name: 'connection_id', - type: 'long', - description: 'Connection identifier.\n', - }, - { - name: 'level', - type: 'alias', - path: 'log.level', - migration: true, - }, - { - name: 'pid', - type: 'alias', - path: 'process.pid', - migration: true, - }, - { - name: 'tid', - type: 'alias', - path: 'process.thread.id', - migration: true, - }, - { - name: 'message', - type: 'alias', - path: 'message', - migration: true, - }, - ], - }, - { - name: 'ingress_controller', - type: 'group', - description: 'Contains fields for the Ingress Nginx controller access logs.\n', - fields: [ - { - name: 'remote_ip_list', - type: 'array', - description: - 'An array of remote IP addresses. It is a list because it is common to include, besides the client IP address, IP addresses from headers like `X-Forwarded-For`. Real source IP is restored to `source.ip`.\n', - }, - { - name: 'http.request.length', - type: 'long', - format: 'bytes', - description: - 'The request length (including request line, header, and request body)\n', - }, - { - name: 'http.request.time', - type: 'double', - format: 'duration', - description: 'Time elapsed since the first bytes were read from the client\n', - }, - { - name: 'upstream.name', - type: 'text', - description: 'The name of the upstream.\n', - }, - { - name: 'upstream.alternative_name', - type: 'text', - description: 'The name of the alternative upstream.\n', - }, - { - name: 'upstream.response.length', - type: 'long', - format: 'bytes', - description: 'The length of the response obtained from the upstream server\n', - }, - { - name: 'upstream.response.time', - type: 'double', - format: 'duration', - description: - 'The time spent on receiving the response from the upstream server as seconds with millisecond resolution\n', - }, - { - name: 'upstream.response.status_code', - type: 'long', - description: 'The status code of the response obtained from the upstream server\n', - }, - { - name: 'http.request.id', - type: 'text', - description: 'The randomly generated ID of the request\n', - }, - { - name: 'upstream.ip', - type: 'ip', - description: - 'The IP address of the upstream server. If several servers were contacted during request processing, their addresses are separated by commas.\n', - }, - { - name: 'upstream.port', - type: 'long', - description: 'The port of the upstream server.\n', - }, - { - name: 'body_sent.bytes', - type: 'alias', - path: 'http.response.body.bytes', - migration: true, - }, - { - name: 'user_name', - type: 'alias', - path: 'user.name', - migration: true, - }, - { - name: 'method', - type: 'alias', - path: 'http.request.method', - migration: true, - }, - { - name: 'url', - type: 'alias', - path: 'url.original', - migration: true, - }, - { - name: 'http_version', - type: 'alias', - path: 'http.version', - migration: true, - }, - { - name: 'response_code', - type: 'alias', - path: 'http.response.status_code', - migration: true, - }, - { - name: 'referrer', - type: 'alias', - path: 'http.request.referrer', - migration: true, - }, - { - name: 'agent', - type: 'alias', - path: 'user_agent.original', - migration: true, - }, - { - name: 'user_agent', - type: 'group', - fields: [ - { - name: 'device', - type: 'alias', - path: 'user_agent.device.name', - migration: true, - }, - { - name: 'name', - type: 'alias', - path: 'user_agent.name', - migration: true, - }, - { - name: 'os', - type: 'alias', - path: 'user_agent.os.full_name', - migration: true, - }, - { - name: 'os_name', - type: 'alias', - path: 'user_agent.os.name', - migration: true, - }, - { - name: 'original', - type: 'alias', - path: 'user_agent.original', - migration: true, - }, - ], - }, - { - name: 'geoip', - type: 'group', - fields: [ - { - name: 'continent_name', - type: 'alias', - path: 'source.geo.continent_name', - migration: true, - }, - { - name: 'country_iso_code', - type: 'alias', - path: 'source.geo.country_iso_code', - migration: true, - }, - { - name: 'location', - type: 'alias', - path: 'source.geo.location', - migration: true, - }, - { - name: 'region_name', - type: 'alias', - path: 'source.geo.region_name', - migration: true, - }, - { - name: 'city_name', - type: 'alias', - path: 'source.geo.city_name', - migration: true, - }, - { - name: 'region_iso_code', - type: 'alias', - path: 'source.geo.region_iso_code', - migration: true, - }, - ], - }, - ], - }, - ], - }, - ], - }, - { - key: 'osquery', - title: 'Osquery', - description: 'Fields exported by the `osquery` module\n', - fields: [ - { - name: 'osquery', - type: 'group', - description: '\n', - fields: [ - { - name: 'result', - type: 'group', - description: 'Common fields exported by the result metricset.\n', - fields: [ - { - name: 'name', - type: 'keyword', - description: 'The name of the query that generated this event.\n', - }, - { - name: 'action', - type: 'keyword', - description: - 'For incremental data, marks whether the entry was added or removed. It can be one of "added", "removed", or "snapshot".\n', - }, - { - name: 'host_identifier', - type: 'keyword', - description: - 'The identifier for the host on which the osquery agent is running. Normally the hostname.\n', - }, - { - name: 'unix_time', - type: 'long', - description: - 'Unix timestamp of the event, in seconds since the epoch. Used for computing the `@timestamp` column.\n', - }, - { - name: 'calendar_time', - type: 'keyword', - description: - 'String representation of the collection time, as formatted by osquery.\n', - }, - ], - }, - ], - }, - ], - }, - { - key: 'postgresql', - title: 'PostgreSQL', - description: 'Module for parsing the PostgreSQL log files.\n', - short_config: true, - fields: [ - { - name: 'postgresql', - type: 'group', - description: 'Fields from PostgreSQL logs.\n', - fields: [ - { - name: 'log', - type: 'group', - description: 'Fields from the PostgreSQL log files.\n', - fields: [ - { - name: 'timestamp', - deprecated: '7.3.0', - description: 'The timestamp from the log line.\n', - }, - { - name: 'core_id', - type: 'long', - description: 'Core id\n', - }, - { - name: 'database', - example: 'mydb', - description: 'Name of database\n', - }, - { - name: 'query', - example: 'SELECT * FROM users;', - description: 'Query statement.\n', - }, - { - name: 'query_step', - example: 'parse', - description: - 'Statement step when using extended query protocol (one of statement, parse, bind or execute)\n', - }, - { - name: 'query_name', - example: 'pdo_stmt_00000001', - description: - 'Name given to a query when using extended query protocol. If it is "", or not present, this field is ignored.\n', - }, - { - name: 'error.code', - type: 'long', - description: 'Error code returned by Postgres (if any)', - }, - { - name: 'timezone', - type: 'alias', - path: 'event.timezone', - migration: true, - }, - { - name: 'thread_id', - type: 'alias', - path: 'process.pid', - migration: true, - }, - { - name: 'user', - type: 'alias', - path: 'user.name', - migration: true, - }, - { - name: 'level', - type: 'alias', - path: 'log.level', - migration: true, - }, - { - name: 'message', - type: 'alias', - path: 'message', - migration: true, - }, - ], - }, - ], - }, - ], - }, - { - key: 'redis', - title: 'Redis', - description: 'Redis Module\n', - fields: [ - { - name: 'redis', - type: 'group', - description: '\n', - fields: [ - { - name: 'log', - type: 'group', - description: 'Redis log files\n', - fields: [ - { - name: 'role', - type: 'keyword', - description: - 'The role of the Redis instance. Can be one of `master`, `slave`, `child` (for RDF/AOF writing child), or `sentinel`.\n', - }, - { - name: 'pid', - type: 'alias', - path: 'process.pid', - migration: true, - }, - { - name: 'level', - type: 'alias', - path: 'log.level', - migration: true, - }, - { - name: 'message', - type: 'alias', - path: 'message', - migration: true, - }, - ], - }, - { - name: 'slowlog', - type: 'group', - description: 'Slow logs are retrieved from Redis via a network connection.\n', - fields: [ - { - name: 'cmd', - type: 'keyword', - description: 'The command executed.\n', - }, - { - name: 'duration.us', - type: 'long', - description: 'How long it took to execute the command in microseconds.\n', - }, - { - name: 'id', - type: 'long', - description: 'The ID of the query.\n', - }, - { - name: 'key', - type: 'keyword', - description: 'The key on which the command was executed.\n', - }, - { - name: 'args', - type: 'keyword', - description: 'The arguments with which the command was called.\n', - }, - ], - }, - ], - }, - ], - }, - { - key: 'santa', - title: 'Google Santa', - description: 'Santa Module\n', - fields: [ - { - name: 'santa', - type: 'group', - description: '\n', - fields: [ - { - name: 'action', - type: 'keyword', - example: 'EXEC', - description: 'Action', - }, - { - name: 'decision', - type: 'keyword', - example: 'ALLOW', - description: 'Decision that santad took.', - }, - { - name: 'reason', - type: 'keyword', - example: 'CERT', - description: 'Reason for the decsision.', - }, - { - name: 'mode', - type: 'keyword', - example: 'M', - description: 'Operating mode of Santa.', - }, - { - name: 'disk', - type: 'group', - description: 'Fields for DISKAPPEAR actions.', - fields: [ - { - name: 'volume', - description: 'The volume name.', - }, - { - name: 'bus', - description: 'The disk bus protocol.', - }, - { - name: 'serial', - description: 'The disk serial number.', - }, - { - name: 'bsdname', - example: 'disk1s3', - description: 'The disk BSD name.', - }, - { - name: 'model', - example: 'APPLE SSD SM0512L', - description: 'The disk model.', - }, - { - name: 'fs', - example: 'apfs', - description: 'The disk volume kind (filesystem type).', - }, - { - name: 'mount', - description: 'The disk volume path.', - }, - ], - }, - ], - }, - { - name: 'certificate.common_name', - type: 'keyword', - description: 'Common name from code signing certificate.', - }, - { - name: 'certificate.sha256', - type: 'keyword', - description: 'SHA256 hash of code signing certificate.', - }, - ], - }, - { - key: 'system', - title: 'System', - description: 'Module for parsing system log files.\n', - short_config: true, - fields: [ - { - name: 'system', - type: 'group', - description: 'Fields from the system log files.\n', - fields: [ - { - name: 'auth', - type: 'group', - description: 'Fields from the Linux authorization logs.\n', - fields: [ - { - name: 'timestamp', - type: 'alias', - path: '@timestamp', - migration: true, - }, - { - name: 'hostname', - type: 'alias', - path: 'host.hostname', - migration: true, - }, - { - name: 'program', - type: 'alias', - path: 'process.name', - migration: true, - }, - { - name: 'pid', - type: 'alias', - path: 'process.pid', - migration: true, - }, - { - name: 'message', - type: 'alias', - path: 'message', - migration: true, - }, - { - name: 'user', - type: 'alias', - path: 'user.name', - migration: true, - }, - { - name: 'ssh', - type: 'group', - fields: [ - { - name: 'method', - description: - 'The SSH authentication method. Can be one of "password" or "publickey".\n', - }, - { - name: 'signature', - description: 'The signature of the client public key.\n', - }, - { - name: 'dropped_ip', - type: 'ip', - description: - 'The client IP from SSH connections that are open and immediately dropped.\n', - }, - { - name: 'event', - example: 'Accepted', - description: - 'The SSH event as found in the logs (Accepted, Invalid, Failed, etc.)\n', - }, - { - name: 'ip', - type: 'alias', - path: 'source.ip', - migration: true, - }, - { - name: 'port', - type: 'alias', - path: 'source.port', - migration: true, - }, - { - name: 'geoip', - type: 'group', - fields: [ - { - name: 'continent_name', - type: 'alias', - path: 'source.geo.continent_name', - migration: true, - }, - { - name: 'country_iso_code', - type: 'alias', - path: 'source.geo.country_iso_code', - migration: true, - }, - { - name: 'location', - type: 'alias', - path: 'source.geo.location', - migration: true, - }, - { - name: 'region_name', - type: 'alias', - path: 'source.geo.region_name', - migration: true, - }, - { - name: 'city_name', - type: 'alias', - path: 'source.geo.city_name', - migration: true, - }, - { - name: 'region_iso_code', - type: 'alias', - path: 'source.geo.region_iso_code', - migration: true, - }, - ], - }, - ], - }, - { - name: 'sudo', - type: 'group', - description: 'Fields specific to events created by the `sudo` command.\n', - fields: [ - { - name: 'error', - example: 'user NOT in sudoers', - description: 'The error message in case the sudo command failed.\n', - }, - { - name: 'tty', - description: 'The TTY where the sudo command is executed.\n', - }, - { - name: 'pwd', - description: 'The current directory where the sudo command is executed.\n', - }, - { - name: 'user', - example: 'root', - description: 'The target user to which the sudo command is switching.\n', - }, - { - name: 'command', - description: 'The command executed via sudo.\n', - }, - ], - }, - { - name: 'useradd', - type: 'group', - description: 'Fields specific to events created by the `useradd` command.\n', - fields: [ - { - name: 'home', - description: 'The home folder for the new user.', - }, - { - name: 'shell', - description: 'The default shell for the new user.', - }, - { - name: 'name', - type: 'alias', - path: 'user.name', - migration: true, - }, - { - name: 'uid', - type: 'alias', - path: 'user.id', - migration: true, - }, - { - name: 'gid', - type: 'alias', - path: 'group.id', - migration: true, - }, - ], - }, - { - name: 'groupadd', - type: 'group', - description: 'Fields specific to events created by the `groupadd` command.\n', - fields: [ - { - name: 'name', - type: 'alias', - path: 'group.name', - migration: true, - }, - { - name: 'gid', - type: 'alias', - path: 'group.id', - migration: true, - }, - ], - }, - ], - }, - { - name: 'syslog', - type: 'group', - description: 'Contains fields from the syslog system logs.\n', - fields: [ - { - name: 'timestamp', - type: 'alias', - path: '@timestamp', - migration: true, - }, - { - name: 'hostname', - type: 'alias', - path: 'host.hostname', - migration: true, - }, - { - name: 'program', - type: 'alias', - path: 'process.name', - migration: true, - }, - { - name: 'pid', - type: 'alias', - path: 'process.pid', - migration: true, - }, - { - name: 'message', - type: 'alias', - path: 'message', - migration: true, - }, - ], - }, - ], - }, - ], - }, - { - key: 'traefik', - title: 'Traefik', - description: 'Module for parsing the Traefik log files.\n', - fields: [ - { - name: 'traefik', - type: 'group', - description: 'Fields from the Traefik log files.\n', - fields: [ - { - name: 'access', - type: 'group', - description: 'Contains fields for the Traefik access logs.\n', - fields: [ - { - name: 'user_identifier', - type: 'keyword', - description: 'Is the RFC 1413 identity of the client\n', - }, - { - name: 'request_count', - type: 'long', - description: 'The number of requests\n', - }, - { - name: 'frontend_name', - type: 'keyword', - description: 'The name of the frontend used\n', - }, - { - name: 'backend_url', - type: 'keyword', - description: 'The url of the backend where request is forwarded', - }, - { - name: 'body_sent.bytes', - type: 'alias', - path: 'http.response.body.bytes', - migration: true, - }, - { - name: 'remote_ip', - type: 'alias', - path: 'source.address', - migration: true, - }, - { - name: 'user_name', - type: 'alias', - path: 'user.name', - migration: true, - }, - { - name: 'method', - type: 'alias', - path: 'http.request.method', - migration: true, - }, - { - name: 'url', - type: 'alias', - path: 'url.original', - migration: true, - }, - { - name: 'http_version', - type: 'alias', - path: 'http.version', - migration: true, - }, - { - name: 'response_code', - type: 'alias', - path: 'http.response.status_code', - migration: true, - }, - { - name: 'referrer', - type: 'alias', - path: 'http.request.referrer', - migration: true, - }, - { - name: 'agent', - type: 'alias', - path: 'user_agent.original', - migration: true, - }, - { - name: 'user_agent', - type: 'group', - fields: [ - { - name: 'device', - type: 'alias', - path: 'user_agent.device.name', - }, - { - name: 'name', - type: 'alias', - path: 'user_agent.name', - }, - { - name: 'os', - type: 'alias', - path: 'user_agent.os.full_name', - }, - { - name: 'os_name', - type: 'alias', - path: 'user_agent.os.name', - }, - { - name: 'original', - type: 'alias', - path: 'user_agent.original', - }, - ], - }, - { - name: 'geoip', - type: 'group', - fields: [ - { - name: 'continent_name', - type: 'alias', - path: 'source.geo.continent_name', - }, - { - name: 'country_iso_code', - type: 'alias', - path: 'source.geo.country_iso_code', - }, - { - name: 'location', - type: 'alias', - path: 'source.geo.location', - }, - { - name: 'region_name', - type: 'alias', - path: 'source.geo.region_name', - }, - { - name: 'city_name', - type: 'alias', - path: 'source.geo.city_name', - }, - { - name: 'region_iso_code', - type: 'alias', - path: 'source.geo.region_iso_code', - }, - ], - }, - ], - }, - ], - }, - ], - }, - { - key: 'activemq', - title: 'activemq', - release: 'ga', - description: 'Module for parsing ActiveMQ log files.\n', - fields: [ - { - name: 'activemq', - type: 'group', - description: '\n', - fields: [ - { - name: 'caller', - type: 'keyword', - description: 'Name of the caller issuing the logging request (class or resource).\n', - }, - { - name: 'thread', - type: 'keyword', - description: 'Thread that generated the logging event.\n', - }, - { - name: 'user', - type: 'keyword', - description: 'User that generated the logging event.\n', - }, - { - name: 'audit', - type: 'group', - description: 'Fields from ActiveMQ audit logs.\n', - fields: [], - }, - { - name: 'log', - type: 'group', - description: 'Fields from ActiveMQ application logs.\n', - fields: [ - { - name: 'stack_trace', - type: 'keyword', - }, - ], - }, - ], - }, - ], - }, - { - key: 'aws', - title: 'AWS', - release: 'beta', - description: 'Module for handling logs from AWS.\n', - fields: [ - { - name: 'aws', - type: 'group', - description: 'Fields from AWS logs.\n', - fields: [ - { - name: 'cloudtrail', - type: 'group', - release: 'beta', - default_field: false, - description: 'Fields for AWS CloudTrail logs.\n', - fields: [ - { - name: 'event_version', - type: 'keyword', - description: 'The CloudTrail version of the log event format.\n', - }, - { - name: 'user_identity', - type: 'group', - description: - 'The userIdentity element contains details about the type of IAM identity that made the request, and which credentials were used. If temporary credentials were used, the element shows how the credentials were obtained.', - fields: [ - { - name: 'type', - type: 'keyword', - description: 'The type of the identity\n', - }, - { - name: 'arn', - type: 'keyword', - description: - 'The Amazon Resource Name (ARN) of the principal that made the call.', - }, - { - name: 'access_key_id', - type: 'keyword', - description: 'The access key ID that was used to sign the request.', - }, - { - name: 'session_context', - type: 'group', - description: - 'If the request was made with temporary security credentials, an element that provides information about the session that was created for those credentials', - fields: [ - { - name: 'mfa_authenticated', - type: 'keyword', - description: - 'The value is true if the root user or IAM user whose credentials were used for the request also was authenticated with an MFA device; otherwise, false.', - }, - { - name: 'creation_date', - type: 'date', - description: - 'The date and time when the temporary security credentials were issued.', - }, - ], - }, - { - name: 'invoked_by', - type: 'keyword', - description: - 'The name of the AWS service that made the request, such as Amazon EC2 Auto Scaling or AWS Elastic Beanstalk.', - }, - { - name: 'session_issuer', - type: 'group', - description: - 'If the request was made with temporary security credentials, an element that provides information about how the credentials were obtained.', - fields: [ - { - name: 'type', - type: 'keyword', - description: - 'The source of the temporary security credentials, such as Root, IAMUser, or Role.', - }, - { - name: 'principal_id', - type: 'keyword', - description: - 'The internal ID of the entity that was used to get credentials.', - }, - { - name: 'arn', - type: 'keyword', - description: - 'The ARN of the source (account, IAM user, or role) that was used to get temporary security credentials.', - }, - { - name: 'account_id', - type: 'keyword', - description: - 'The account that owns the entity that was used to get credentials.', - }, - ], - }, - ], - }, - { - name: 'error_code', - type: 'keyword', - description: 'The AWS service error if the request returns an error.', - }, - { - name: 'error_message', - type: 'keyword', - description: 'If the request returns an error, the description of the error.', - }, - { - name: 'request_parameters', - type: 'keyword', - description: 'The parameters, if any, that were sent with the request.', - }, - { - name: 'response_elements', - type: 'keyword', - description: - 'The response element for actions that make changes (create, update, or delete actions).', - }, - { - name: 'additional_eventdata', - type: 'keyword', - description: - 'Additional data about the event that was not part of the request or response.', - }, - { - name: 'request_id', - type: 'keyword', - description: - 'The value that identifies the request. The service being called generates this value.', - }, - { - name: 'event_type', - type: 'keyword', - description: 'Identifies the type of event that generated the event record.', - }, - { - name: 'api_version', - type: 'keyword', - description: - 'Identifies the API version associated with the AwsApiCall eventType value.', - }, - { - name: 'management_event', - type: 'keyword', - description: - 'A Boolean value that identifies whether the event is a management event.', - }, - { - name: 'read_only', - type: 'keyword', - description: 'Identifies whether this operation is a read-only operation.', - }, - { - name: 'resources', - type: 'group', - description: 'A list of resources accessed in the event.', - fields: [ - { - name: 'arn', - type: 'keyword', - description: 'Resource ARNs', - }, - { - name: 'account_id', - type: 'keyword', - description: 'Account ID of the resource owner', - }, - { - name: 'type', - type: 'keyword', - description: - 'Resource type identifier in the format: AWS::aws-service-name::data-type-name', - }, - ], - }, - { - name: 'recipient_account_id', - type: 'keyword', - description: 'Represents the account ID that received this event.', - }, - { - name: 'service_event_details', - type: 'keyword', - description: - 'Identifies the service event, including what triggered the event and the result.', - }, - { - name: 'shared_event_id', - type: 'keyword', - description: - 'GUID generated by CloudTrail to uniquely identify CloudTrail events from the same AWS action that is sent to different AWS accounts.', - }, - { - name: 'vpc_endpoint_id', - type: 'keyword', - description: - 'Identifies the VPC endpoint in which requests were made from a VPC to another AWS service, such as Amazon S3.', - }, - { - name: 'console_login', - type: 'group', - description: 'Fields specific to ConsoleLogin events', - fields: [ - { - name: 'additional_eventdata', - type: 'group', - description: 'Additional Event Data for ConsoleLogin events\n', - fields: [ - { - name: 'mobile_version', - type: 'boolean', - description: 'Identifies whether ConsoleLogin was from mobile version', - }, - { - name: 'login_to', - type: 'keyword', - description: 'URL for ConsoleLogin', - }, - { - name: 'mfa_used', - type: 'boolean', - description: - 'Identifies whether multi factor authentication was used during ConsoleLogin', - }, - ], - }, - ], - }, - ], - }, - { - name: 'cloudwatch', - type: 'group', - release: 'beta', - default_field: false, - description: 'Fields for AWS CloudWatch logs.\n', - fields: [], - }, - { - name: 'ec2', - type: 'group', - release: 'beta', - default_field: false, - description: 'Fields for AWS EC2 logs in CloudWatch.\n', - fields: [ - { - name: 'ip_address', - type: 'keyword', - description: 'The internet address of the requester.\n', - }, - ], - }, - { - name: 'elb', - type: 'group', - release: 'ga', - description: 'Fields for AWS ELB logs.\n', - fields: [ - { - name: 'name', - type: 'keyword', - description: 'The name of the load balancer.\n', - }, - { - name: 'type', - type: 'keyword', - description: 'The type of the load balancer for v2 Load Balancers.\n', - }, - { - name: 'target_group.arn', - type: 'keyword', - description: 'The ARN of the target group handling the request.\n', - }, - { - name: 'listener', - type: 'keyword', - description: 'The ELB listener that received the connection.\n', - }, - { - name: 'protocol', - type: 'keyword', - description: 'The protocol of the load balancer (http or tcp).\n', - }, - { - name: 'request_processing_time.sec', - type: 'float', - description: - 'The total time in seconds since the connection or request is received until it is sent to a registered backend.\n', - }, - { - name: 'backend_processing_time.sec', - type: 'float', - description: - 'The total time in seconds since the connection is sent to the backend till the backend starts responding.\n', - }, - { - name: 'response_processing_time.sec', - type: 'float', - description: - 'The total time in seconds since the response is received from the backend till it is sent to the client.\n', - }, - { - name: 'connection_time.ms', - type: 'long', - description: - 'The total time of the connection in milliseconds, since it is opened till it is closed.\n', - }, - { - name: 'tls_handshake_time.ms', - type: 'long', - description: - 'The total time for the TLS handshake to complete in milliseconds once the connection has been established.\n', - }, - { - name: 'backend.ip', - type: 'keyword', - description: 'The IP address of the backend processing this connection.\n', - }, - { - name: 'backend.port', - type: 'keyword', - description: 'The port in the backend processing this connection.\n', - }, - { - name: 'backend.http.response.status_code', - type: 'keyword', - description: - 'The status code from the backend (status code sent to the client from ELB is stored in `http.response.status_code`\n', - }, - { - name: 'ssl_cipher', - type: 'keyword', - description: 'The SSL cipher used in TLS/SSL connections.\n', - }, - { - name: 'ssl_protocol', - type: 'keyword', - description: 'The SSL protocol used in TLS/SSL connections.\n', - }, - { - name: 'chosen_cert.arn', - type: 'keyword', - description: - 'The ARN of the chosen certificate presented to the client in TLS/SSL connections.\n', - }, - { - name: 'chosen_cert.serial', - type: 'keyword', - description: - 'The serial number of the chosen certificate presented to the client in TLS/SSL connections.\n', - }, - { - name: 'incoming_tls_alert', - type: 'keyword', - description: - 'The integer value of TLS alerts received by the load balancer from the client, if present.\n', - }, - { - name: 'tls_named_group', - type: 'keyword', - description: 'The TLS named group.\n', - }, - { - name: 'trace_id', - type: 'keyword', - description: 'The contents of the `X-Amzn-Trace-Id` header.\n', - }, - { - name: 'matched_rule_priority', - type: 'keyword', - description: - 'The priority value of the rule that matched the request, if a rule matched.\n', - }, - { - name: 'action_executed', - type: 'keyword', - description: - 'The action executed when processing the request (forward, fixed-response, authenticate...). It can contain several values.\n', - }, - { - name: 'redirect_url', - type: 'keyword', - description: 'The URL used if a redirection action was executed.\n', - }, - { - name: 'error.reason', - type: 'keyword', - description: 'The error reason if the executed action failed.', - }, - ], - }, - { - name: 's3access', - type: 'group', - release: 'ga', - description: 'Fields for AWS S3 server access logs.\n', - fields: [ - { - name: 'bucket_owner', - type: 'keyword', - description: 'The canonical user ID of the owner of the source bucket.\n', - }, - { - name: 'bucket', - type: 'keyword', - description: 'The name of the bucket that the request was processed against.\n', - }, - { - name: 'remote_ip', - type: 'ip', - description: 'The apparent internet address of the requester.\n', - }, - { - name: 'requester', - type: 'keyword', - description: - 'The canonical user ID of the requester, or a - for unauthenticated requests.\n', - }, - { - name: 'request_id', - type: 'keyword', - description: 'A string generated by Amazon S3 to uniquely identify each request.\n', - }, - { - name: 'operation', - type: 'keyword', - description: - 'The operation listed here is declared as SOAP.operation, REST.HTTP_method.resource_type, WEBSITE.HTTP_method.resource_type, or BATCH.DELETE.OBJECT.\n', - }, - { - name: 'key', - type: 'keyword', - description: - 'The "key" part of the request, URL encoded, or "-" if the operation does not take a key parameter.\n', - }, - { - name: 'request_uri', - type: 'keyword', - description: 'The Request-URI part of the HTTP request message.\n', - }, - { - name: 'http_status', - type: 'long', - description: 'The numeric HTTP status code of the response.\n', - }, - { - name: 'error_code', - type: 'keyword', - description: 'The Amazon S3 Error Code, or "-" if no error occurred.\n', - }, - { - name: 'bytes_sent', - type: 'long', - description: - 'The number of response bytes sent, excluding HTTP protocol overhead, or "-" if zero.\n', - }, - { - name: 'object_size', - type: 'long', - description: 'The total size of the object in question.\n', - }, - { - name: 'total_time', - type: 'long', - description: - "The number of milliseconds the request was in flight from the server's perspective.\n", - }, - { - name: 'turn_around_time', - type: 'long', - description: - 'The number of milliseconds that Amazon S3 spent processing your request.\n', - }, - { - name: 'referrer', - type: 'keyword', - description: 'The value of the HTTP Referrer header, if present.\n', - }, - { - name: 'user_agent', - type: 'keyword', - description: 'The value of the HTTP User-Agent header.\n', - }, - { - name: 'version_id', - type: 'keyword', - description: - 'The version ID in the request, or "-" if the operation does not take a versionId parameter.\n', - }, - { - name: 'host_id', - type: 'keyword', - description: 'The x-amz-id-2 or Amazon S3 extended request ID.\n', - }, - { - name: 'signature_version', - type: 'keyword', - description: - 'The signature version, SigV2 or SigV4, that was used to authenticate the request or a - for unauthenticated requests.\n', - }, - { - name: 'cipher_suite', - type: 'keyword', - description: - 'The Secure Sockets Layer (SSL) cipher that was negotiated for HTTPS request or a - for HTTP.\n', - }, - { - name: 'authentication_type', - type: 'keyword', - description: - 'The type of request authentication used, AuthHeader for authentication headers, QueryString for query string (pre-signed URL) or a - for unauthenticated requests.\n', - }, - { - name: 'host_header', - type: 'keyword', - description: 'The endpoint used to connect to Amazon S3.\n', - }, - { - name: 'tls_version', - type: 'keyword', - description: - 'The Transport Layer Security (TLS) version negotiated by the client.\n', - }, - ], - }, - { - name: 'vpcflow', - type: 'group', - release: 'beta', - description: 'Fields for AWS VPC flow logs.\n', - fields: [ - { - name: 'version', - type: 'keyword', - description: - 'The VPC Flow Logs version. If you use the default format, the version is 2. If you specify a custom format, the version is 3.\n', - }, - { - name: 'account_id', - type: 'keyword', - description: 'The AWS account ID for the flow log.\n', - }, - { - name: 'interface_id', - type: 'keyword', - description: 'The ID of the network interface for which the traffic is recorded.\n', - }, - { - name: 'action', - type: 'keyword', - description: 'The action that is associated with the traffic, ACCEPT or REJECT.\n', - }, - { - name: 'log_status', - type: 'keyword', - description: 'The logging status of the flow log, OK, NODATA or SKIPDATA.\n', - }, - { - name: 'instance_id', - type: 'keyword', - description: - "The ID of the instance that's associated with network interface for which the traffic is recorded, if the instance is owned by you.\n", - }, - { - name: 'pkt_srcaddr', - type: 'ip', - description: 'The packet-level (original) source IP address of the traffic.\n', - }, - { - name: 'pkt_dstaddr', - type: 'ip', - description: - 'The packet-level (original) destination IP address for the traffic.\n', - }, - { - name: 'vpc_id', - type: 'keyword', - description: - 'The ID of the VPC that contains the network interface for which the traffic is recorded.\n', - }, - { - name: 'subnet_id', - type: 'keyword', - description: - 'The ID of the subnet that contains the network interface for which the traffic is recorded.\n', - }, - { - name: 'tcp_flags', - type: 'keyword', - description: - 'The bitmask value for the following TCP flags: 2=SYN,18=SYN-ACK,1=FIN,4=RST\n', - }, - { - name: 'type', - type: 'keyword', - description: 'The type of traffic: IPv4, IPv6, or EFA.\n', - }, - ], - }, - ], - }, - ], - }, - { - key: 'azure', - title: 'Azure', - release: 'beta', - description: 'Azure Module\n', - fields: [ - { - name: 'azure', - type: 'group', - description: '\n', - fields: [ - { - name: 'subscription_id', - type: 'keyword', - description: 'Azure subscription ID\n', - }, - { - name: 'correlation_id', - type: 'keyword', - description: 'Correlation ID\n', - }, - { - name: 'tenant_id', - type: 'keyword', - description: 'tenant ID\n', - }, - { - name: 'resource', - type: 'group', - description: 'Resource\n', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'Resource ID\n', - }, - { - name: 'group', - type: 'keyword', - description: 'Resource group\n', - }, - { - name: 'provider', - type: 'keyword', - description: 'Resource type/namespace\n', - }, - { - name: 'namespace', - type: 'keyword', - description: 'Resource type/namespace\n', - }, - { - name: 'name', - type: 'keyword', - description: 'Name\n', - }, - { - name: 'authorization_rule', - type: 'keyword', - description: 'Authorization rule\n', - }, - ], - }, - { - name: 'activitylogs', - type: 'group', - release: 'beta', - description: 'Fields for Azure activity logs.\n', - fields: [ - { - name: 'identity', - type: 'group', - description: 'Identity\n', - fields: [ - { - name: 'claims_initiated_by_user', - type: 'group', - description: 'Claims initiated by user\n', - fields: [ - { - name: 'name', - type: 'keyword', - description: 'Name\n', - }, - { - name: 'givenname', - type: 'keyword', - description: 'Givenname\n', - }, - { - name: 'surname', - type: 'keyword', - description: 'Surname\n', - }, - { - name: 'fullname', - type: 'keyword', - description: 'Fullname\n', - }, - { - name: 'schema', - type: 'keyword', - description: 'Schema\n', - }, - ], - }, - { - name: 'claims.*', - type: 'object', - object_type: 'keyword', - object_type_mapping_type: '*', - description: 'Claims\n', - }, - { - name: 'authorization', - type: 'group', - description: 'Authorization\n', - fields: [ - { - name: 'scope', - type: 'keyword', - description: 'Scope\n', - }, - { - name: 'action', - type: 'keyword', - description: 'Action\n', - }, - { - name: 'evidence', - type: 'group', - description: 'Evidence\n', - fields: [ - { - name: 'role_assignment_scope', - type: 'keyword', - description: 'Role assignment scope\n', - }, - { - name: 'role_definition_id', - type: 'keyword', - description: 'Role definition ID\n', - }, - { - name: 'role', - type: 'keyword', - description: 'Role\n', - }, - { - name: 'role_assignment_id', - type: 'keyword', - description: 'Role assignment ID\n', - }, - { - name: 'principal_id', - type: 'keyword', - description: 'Principal ID\n', - }, - { - name: 'principal_type', - type: 'keyword', - description: 'Principal type\n', - }, - ], - }, - ], - }, - ], - }, - { - name: 'operation_name', - type: 'keyword', - description: 'Operation name\n', - }, - { - name: 'result_signature', - type: 'keyword', - description: 'Result signature\n', - }, - { - name: 'category', - type: 'keyword', - description: 'Category\n', - }, - { - name: 'properties', - type: 'group', - description: 'Properties\n', - fields: [ - { - name: 'service_request_id', - type: 'keyword', - description: 'Service Request Id\n', - }, - { - name: 'status_code', - type: 'keyword', - description: 'Status code\n', - }, - ], - }, - ], - }, - { - name: 'auditlogs', - type: 'group', - description: 'Fields for Azure audit logs.\n', - fields: [ - { - name: 'operation_name', - type: 'keyword', - description: 'The operation name\n', - }, - { - name: 'operation_version', - type: 'keyword', - description: 'The operation version\n', - }, - { - name: 'identity', - type: 'keyword', - description: 'Identity\n', - }, - { - name: 'tenant_id', - type: 'keyword', - description: 'Tenant ID\n', - }, - { - name: 'result_signature', - type: 'keyword', - description: 'Result signature\n', - }, - { - name: 'properties', - type: 'group', - description: 'The audit log properties\n', - fields: [ - { - name: 'result', - type: 'keyword', - description: 'Log result\n', - }, - { - name: 'activity_display_name', - type: 'keyword', - description: 'Activity display name\n', - }, - { - name: 'result_reason', - type: 'keyword', - description: 'Reason for the log result\n', - }, - { - name: 'correlation_id', - type: 'keyword', - description: 'Correlation ID\n', - }, - { - name: 'logged_by_service', - type: 'keyword', - description: 'Logged by service\n', - }, - { - name: 'operation_type', - type: 'keyword', - description: 'Operation type\n', - }, - { - name: 'id', - type: 'keyword', - description: 'ID\n', - }, - { - name: 'activity_datetime', - type: 'date', - description: 'Activity timestamp\n', - }, - { - name: 'category', - type: 'keyword', - description: 'category\n', - }, - { - name: 'target_resources.*', - type: 'group', - object_type_mapping_type: '*', - description: 'Target resources\n', - fields: [ - { - name: 'display_name', - type: 'keyword', - description: 'Display name\n', - }, - { - name: 'id', - type: 'keyword', - description: 'ID\n', - }, - { - name: 'type', - type: 'keyword', - description: 'Type\n', - }, - { - name: 'ip_address', - type: 'keyword', - description: 'ip Address\n', - }, - { - name: 'user_principal_name', - type: 'keyword', - description: 'User principal name\n', - }, - { - name: 'modified_properties.*', - type: 'group', - object_type: 'keyword', - object_type_mapping_type: '*', - description: 'Modified properties\n', - fields: [ - { - name: 'new_value', - type: 'keyword', - description: 'New value\n', - }, - { - name: 'display_name', - type: 'keyword', - description: 'Display value\n', - }, - { - name: 'old_value', - type: 'keyword', - description: 'Old value\n', - }, - ], - }, - ], - }, - { - name: 'initiated_by', - type: 'group', - description: 'Information regarding the initiator\n', - fields: [ - { - name: 'app', - type: 'group', - description: 'App\n', - fields: [ - { - name: 'servicePrincipalName', - type: 'keyword', - description: 'Service principal name\n', - }, - { - name: 'displayName', - type: 'keyword', - description: 'Display name\n', - }, - { - name: 'appId', - type: 'keyword', - description: 'App ID\n', - }, - { - name: 'servicePrincipalId', - type: 'keyword', - description: 'Service principal ID\n', - }, - ], - }, - { - name: 'user', - type: 'group', - description: 'User\n', - fields: [ - { - name: 'userPrincipalName', - type: 'keyword', - description: 'User principal name\n', - }, - { - name: 'displayName', - type: 'keyword', - description: 'Display name\n', - }, - { - name: 'id', - type: 'keyword', - description: 'ID\n', - }, - { - name: 'ipAddress', - type: 'keyword', - description: 'ip Address\n', - }, - ], - }, - ], - }, - ], - }, - ], - }, - { - name: 'signinlogs', - type: 'group', - description: 'Fields for Azure sign-in logs.\n', - fields: [ - { - name: 'operation_name', - type: 'keyword', - description: 'The operation name\n', - }, - { - name: 'operation_version', - type: 'keyword', - description: 'The operation version\n', - }, - { - name: 'tenant_id', - type: 'keyword', - description: 'Tenant ID\n', - }, - { - name: 'result_signature', - type: 'keyword', - description: 'Result signature\n', - }, - { - name: 'result_description', - type: 'keyword', - description: 'Result description\n', - }, - { - name: 'identity', - type: 'keyword', - description: 'Identity\n', - }, - { - name: 'properties', - type: 'group', - description: 'The signin log properties\n', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'ID\n', - }, - { - name: 'created_at', - type: 'date', - description: 'Created date time\n', - }, - { - name: 'user_display_name', - type: 'keyword', - description: 'User display name\n', - }, - { - name: 'correlation_id', - type: 'keyword', - description: 'Correlation ID\n', - }, - { - name: 'user_principal_name', - type: 'keyword', - description: 'User principal name\n', - }, - { - name: 'user_id', - type: 'keyword', - description: 'User ID\n', - }, - { - name: 'app_id', - type: 'keyword', - description: 'App ID\n', - }, - { - name: 'app_display_name', - type: 'keyword', - description: 'App display name\n', - }, - { - name: 'ip_address', - type: 'keyword', - description: 'Ip address\n', - }, - { - name: 'client_app_used', - type: 'keyword', - description: 'Client app used\n', - }, - { - name: 'conditional_access_status', - type: 'keyword', - description: 'Conditional access status\n', - }, - { - name: 'original_request_id', - type: 'keyword', - description: 'Original request ID\n', - }, - { - name: 'is_interactive', - type: 'keyword', - description: 'Is interactive\n', - }, - { - name: 'token_issuer_name', - type: 'keyword', - description: 'Token issuer name\n', - }, - { - name: 'token_issuer_type', - type: 'keyword', - description: 'Token issuer type\n', - }, - { - name: 'processing_time_ms', - type: 'float', - description: 'Processing time in milliseconds\n', - }, - { - name: 'risk_detail', - type: 'keyword', - description: 'Risk detail\n', - }, - { - name: 'risk_level_aggregated', - type: 'keyword', - description: 'Risk level aggregated\n', - }, - { - name: 'risk_level_during_signin', - type: 'keyword', - description: 'Risk level during signIn\n', - }, - { - name: 'risk_state', - type: 'keyword', - description: 'Risk state\n', - }, - { - name: 'resource_display_name', - type: 'keyword', - description: 'Resource display name\n', - }, - { - name: 'status', - type: 'group', - description: 'Status\n', - fields: [ - { - name: 'error_code', - type: 'keyword', - description: 'Error code\n', - }, - ], - }, - { - name: 'device_detail', - type: 'group', - description: 'Status\n', - fields: [ - { - name: 'device_id', - type: 'keyword', - description: 'Device ID\n', - }, - { - name: 'operating_system', - type: 'keyword', - description: 'Operating system\n', - }, - { - name: 'browser', - type: 'keyword', - description: 'Browser\n', - }, - { - name: 'display_name', - type: 'keyword', - description: 'Display name\n', - }, - { - name: 'trust_type', - type: 'keyword', - description: 'Trust type\n', - }, - ], - }, - { - name: 'service_principal_id', - type: 'keyword', - description: 'Status\n', - }, - ], - }, - ], - }, - ], - }, - ], - }, - { - key: 'cef-module', - title: 'CEF', - description: - 'Module for receiving CEF logs over Syslog. The module adds vendor specific fields in addition to the fields the decode_cef processor provides.\n', - fields: [ - { - name: 'forcepoint', - type: 'group', - default_field: false, - description: 'Fields for Forcepoint Custom String mappings\n', - fields: [ - { - name: 'virus_id', - type: 'keyword', - description: 'Virus ID\n', - }, - ], - }, - { - name: 'checkpoint', - type: 'group', - default_field: false, - description: 'Fields for Check Point custom string mappings.\n', - fields: [ - { - name: 'app_risk', - type: 'keyword', - description: 'Application risk.', - }, - { - name: 'app_severity', - type: 'keyword', - description: 'Application threat severity.', - }, - { - name: 'app_sig_id', - type: 'keyword', - description: 'The signature ID which the application was detected by.', - }, - { - name: 'auth_method', - type: 'keyword', - description: 'Password authentication protocol used.', - }, - { - name: 'category', - type: 'keyword', - description: 'Category.', - }, - { - name: 'confidence_level', - type: 'keyword', - description: 'Confidence level determined.', - }, - { - name: 'connectivity_state', - type: 'keyword', - description: 'Connectivity state.', - }, - { - name: 'cookie', - type: 'keyword', - description: 'IKE cookie.', - }, - { - name: 'dst_phone_number', - type: 'keyword', - description: 'Destination IP-Phone.', - }, - { - name: 'email_control', - type: 'keyword', - description: 'Engine name.', - }, - { - name: 'email_id', - type: 'keyword', - description: 'Internal email ID.', - }, - { - name: 'email_recipients_num', - type: 'long', - description: 'Number of recipients.', - }, - { - name: 'email_session_id', - type: 'keyword', - description: 'Internal email session ID.', - }, - { - name: 'email_spool_id', - type: 'keyword', - description: 'Internal email spool ID.', - }, - { - name: 'email_subject', - type: 'keyword', - description: 'Email subject.', - }, - { - name: 'event_count', - type: 'long', - description: 'Number of events associated with the log.', - }, - { - name: 'file_hash', - type: 'keyword', - description: 'File hash (SHA1 or MD5).', - }, - { - name: 'frequency', - type: 'keyword', - description: 'Scan frequency.', - }, - { - name: 'icmp_type', - type: 'long', - description: 'ICMP type.', - }, - { - name: 'icmp_code', - type: 'long', - description: 'ICMP code.', - }, - { - name: 'identity_type', - type: 'keyword', - description: 'Identity type.', - }, - { - name: 'incident_extension', - type: 'keyword', - description: 'Format of original data.', - }, - { - name: 'integrity_av_invoke_type', - type: 'keyword', - description: 'Scan invoke type.', - }, - { - name: 'peer_gateway', - type: 'ip', - description: 'Main IP of the peer Security Gateway.', - }, - { - name: 'performance_impact', - type: 'keyword', - description: 'Protection performance impact.', - }, - { - name: 'protection_id', - type: 'keyword', - description: 'Protection malware ID.', - }, - { - name: 'protection_name', - type: 'keyword', - description: 'Specific signature name of the attack.', - }, - { - name: 'protection_type', - type: 'keyword', - description: 'Type of protection used to detect the attack.', - }, - { - name: 'scan_result', - type: 'keyword', - description: 'Scan result.', - }, - { - name: 'sensor_mode', - type: 'keyword', - description: 'Sensor mode.', - }, - { - name: 'severity', - type: 'keyword', - description: 'Threat severity.', - }, - { - name: 'malware_status', - type: 'keyword', - description: 'Malware status.', - }, - { - name: 'subscription_expiration', - type: 'date', - description: 'The expiration date of the subscription.', - }, - { - name: 'tcp_flags', - type: 'keyword', - description: 'TCP packet flags.', - }, - { - name: 'termination_reason', - type: 'keyword', - description: 'Termination reason.', - }, - { - name: 'update_status', - type: 'keyword', - description: 'Update status.', - }, - { - name: 'user_status', - type: 'keyword', - description: 'User response.', - }, - { - name: 'uuid', - type: 'keyword', - description: 'External ID.', - }, - { - name: 'virus_name', - type: 'keyword', - description: 'Virus name.', - }, - { - name: 'malware_name', - type: 'keyword', - description: 'Malware name.', - }, - { - name: 'malware_family', - type: 'keyword', - description: 'Malware family.', - }, - { - name: 'voip_log_type', - type: 'keyword', - description: 'VoIP log types.', - }, - ], - }, - { - name: 'cef.extensions', - type: 'group', - default_field: false, - description: 'Extra vendor-specific extensions.\n', - fields: [ - { - name: 'cp_app_risk', - type: 'keyword', - }, - { - name: 'cp_severity', - type: 'keyword', - }, - { - name: 'ifname', - type: 'keyword', - }, - { - name: 'inzone', - type: 'keyword', - }, - { - name: 'layer_uuid', - type: 'keyword', - }, - { - name: 'layer_name', - type: 'keyword', - }, - { - name: 'logid', - type: 'keyword', - }, - { - name: 'loguid', - type: 'keyword', - }, - { - name: 'match_id', - type: 'keyword', - }, - { - name: 'nat_addtnl_rulenum', - type: 'keyword', - }, - { - name: 'nat_rulenum', - type: 'keyword', - }, - { - name: 'origin', - type: 'keyword', - }, - { - name: 'originsicname', - type: 'keyword', - }, - { - name: 'outzone', - type: 'keyword', - }, - { - name: 'parent_rule', - type: 'keyword', - }, - { - name: 'product', - type: 'keyword', - }, - { - name: 'rule_action', - type: 'keyword', - }, - { - name: 'rule_uid', - type: 'keyword', - }, - { - name: 'sequencenum', - type: 'keyword', - }, - { - name: 'service_id', - type: 'keyword', - }, - { - name: 'version', - type: 'keyword', - }, - ], - }, - ], - }, - { - key: 'cisco', - title: 'Cisco', - description: 'Module for handling Cisco network device logs.\n', - fields: [ - { - name: 'cisco', - type: 'group', - description: 'Fields from Cisco logs.\n', - fields: [ - { - name: 'asa', - type: 'group', - description: 'Fields for Cisco ASA Firewall.\n', - fields: [ - { - name: 'message_id', - type: 'keyword', - description: 'The Cisco ASA message identifier.\n', - }, - { - name: 'suffix', - type: 'keyword', - example: 'session', - description: 'Optional suffix after %ASA identifier.\n', - }, - { - name: 'source_interface', - type: 'keyword', - description: 'Source interface for the flow or event.\n', - }, - { - name: 'destination_interface', - type: 'keyword', - description: 'Destination interface for the flow or event.\n', - }, - { - name: 'rule_name', - type: 'keyword', - description: 'Name of the Access Control List rule that matched this event.\n', - }, - { - name: 'source_username', - type: 'keyword', - description: 'Name of the user that is the source for this event.\n', - }, - { - name: 'destination_username', - type: 'keyword', - description: 'Name of the user that is the destination for this event.\n', - }, - { - name: 'mapped_source_ip', - type: 'ip', - description: 'The translated source IP address.\n', - }, - { - name: 'mapped_source_port', - type: 'long', - description: 'The translated source port.\n', - }, - { - name: 'mapped_destination_ip', - type: 'ip', - description: 'The translated destination IP address.\n', - }, - { - name: 'mapped_destination_port', - type: 'long', - description: 'The translated destination port.\n', - }, - { - name: 'threat_level', - type: 'keyword', - description: - 'Threat level for malware / botnet traffic. One of very-low, low, moderate, high or very-high.\n', - }, - { - name: 'threat_category', - type: 'keyword', - description: - 'Category for the malware / botnet traffic. For example: virus, botnet, trojan, etc.\n', - }, - { - name: 'connection_id', - type: 'keyword', - description: 'Unique identifier for a flow.\n', - }, - { - name: 'icmp_type', - type: 'short', - description: 'ICMP type.\n', - }, - { - name: 'icmp_code', - type: 'short', - description: 'ICMP code.\n', - }, - { - name: 'connection_type', - type: 'keyword', - default_field: false, - description: 'The VPN connection type\n', - }, - { - name: 'dap_records', - default_field: false, - type: 'keyword', - description: 'The assigned DAP records\n', - }, - ], - }, - { - name: 'ftd', - type: 'group', - description: 'Fields for Cisco Firepower Threat Defense Firewall.\n', - fields: [ - { - name: 'message_id', - type: 'keyword', - description: 'The Cisco FTD message identifier.\n', - }, - { - name: 'suffix', - type: 'keyword', - example: 'session', - description: 'Optional suffix after %FTD identifier.\n', - }, - { - name: 'source_interface', - type: 'keyword', - description: 'Source interface for the flow or event.\n', - }, - { - name: 'destination_interface', - type: 'keyword', - description: 'Destination interface for the flow or event.\n', - }, - { - name: 'rule_name', - type: 'keyword', - description: 'Name of the Access Control List rule that matched this event.\n', - }, - { - name: 'source_username', - type: 'keyword', - description: 'Name of the user that is the source for this event.\n', - }, - { - name: 'destination_username', - type: 'keyword', - description: 'Name of the user that is the destination for this event.\n', - }, - { - name: 'mapped_source_ip', - type: 'ip', - description: 'The translated source IP address. Use ECS source.nat.ip.\n', - }, - { - name: 'mapped_source_port', - type: 'long', - description: 'The translated source port. Use ECS source.nat.port.\n', - }, - { - name: 'mapped_destination_ip', - type: 'ip', - description: 'The translated destination IP address. Use ECS destination.nat.ip.\n', - }, - { - name: 'mapped_destination_port', - type: 'long', - description: 'The translated destination port. Use ECS destination.nat.port.\n', - }, - { - name: 'threat_level', - type: 'keyword', - description: - 'Threat level for malware / botnet traffic. One of very-low, low, moderate, high or very-high.\n', - }, - { - name: 'threat_category', - type: 'keyword', - description: - 'Category for the malware / botnet traffic. For example: virus, botnet, trojan, etc.\n', - }, - { - name: 'connection_id', - type: 'keyword', - description: 'Unique identifier for a flow.\n', - }, - { - name: 'icmp_type', - type: 'short', - description: 'ICMP type.\n', - }, - { - name: 'icmp_code', - type: 'short', - description: 'ICMP code.\n', - }, - { - name: 'security', - type: 'object', - description: 'Raw fields for Security Events.', - }, - { - name: 'connection_type', - type: 'keyword', - default_field: false, - description: 'The VPN connection type\n', - }, - { - name: 'dap_records', - type: 'keyword', - default_field: false, - description: 'The assigned DAP records\n', - }, - ], - }, - { - name: 'ios', - type: 'group', - description: 'Fields for Cisco IOS logs.\n', - fields: [ - { - name: 'access_list', - type: 'keyword', - description: 'Name of the IP access list.\n', - }, - { - name: 'facility', - type: 'keyword', - example: 'SEC', - description: - 'The facility to which the message refers (for example, SNMP, SYS, and so forth). A facility can be a hardware device, a protocol, or a module of the system software. It denotes the source or the cause of the system message.\n', - }, - ], - }, - ], - }, - ], - }, - { - key: 'coredns', - title: 'Coredns', - description: 'Module for handling logs produced by coredns\n', - fields: [ - { - name: 'coredns', - type: 'group', - description: 'coredns fields after normalization\n', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'id of the DNS transaction\n', - }, - { - name: 'query.size', - type: 'integer', - format: 'bytes', - description: 'size of the DNS query\n', - }, - { - name: 'query.class', - type: 'keyword', - description: 'DNS query class\n', - }, - { - name: 'query.name', - type: 'keyword', - description: 'DNS query name\n', - }, - { - name: 'query.type', - type: 'keyword', - description: 'DNS query type\n', - }, - { - name: 'response.code', - type: 'keyword', - description: 'DNS response code\n', - }, - { - name: 'response.flags', - type: 'keyword', - description: 'DNS response flags\n', - }, - { - name: 'response.size', - type: 'integer', - format: 'bytes', - description: 'size of the DNS response\n', - }, - { - name: 'dnssec_ok', - type: 'boolean', - description: 'dnssec flag\n', - }, - ], - }, - ], - }, - { - key: 'envoyproxy', - title: 'Envoyproxy', - description: 'Module for handling logs produced by envoy\n', - fields: [ - { - name: 'envoyproxy', - type: 'group', - description: 'Fields from envoy proxy logs after normalization\n', - fields: [ - { - name: 'log_type', - type: 'keyword', - description: 'Envoy log type, normally ACCESS\n', - }, - { - name: 'response_flags', - type: 'keyword', - description: 'Response flags\n', - }, - { - name: 'upstream_service_time', - type: 'long', - format: 'duration', - input_format: 'nanoseconds', - description: 'Upstream service time in nanoseconds\n', - }, - { - name: 'request_id', - type: 'keyword', - description: 'ID of the request\n', - }, - { - name: 'authority', - type: 'keyword', - description: 'Envoy proxy authority field\n', - }, - { - name: 'proxy_type', - type: 'keyword', - description: 'Envoy proxy type, tcp or http\n', - }, - ], - }, - ], - }, - { - key: 'googlecloud', - title: 'Google Cloud', - description: 'Module for handling logs from Google Cloud.\n', - fields: [ - { - name: 'googlecloud', - type: 'group', - description: 'Fields from Google Cloud logs.\n', - fields: [ - { - name: 'destination.instance', - type: 'group', - description: - 'If the destination of the connection was a VM located on the same VPC, this field is populated with VM instance details. In a Shared VPC configuration, project_id corresponds to the project that owns the instance, usually the service project.\n', - fields: [ - { - name: 'project_id', - type: 'keyword', - description: 'ID of the project containing the VM.\n', - }, - { - name: 'region', - type: 'keyword', - description: 'Region of the VM.\n', - }, - { - name: 'zone', - type: 'keyword', - description: 'Zone of the VM.\n', - }, - ], - }, - { - name: 'destination.vpc', - type: 'group', - description: - 'If the destination of the connection was a VM located on the same VPC, this field is populated with VPC network details. In a Shared VPC configuration, project_id corresponds to that of the host project.\n', - fields: [ - { - name: 'project_id', - type: 'keyword', - description: 'ID of the project containing the VM.\n', - }, - { - name: 'vpc_name', - type: 'keyword', - description: 'VPC on which the VM is operating.\n', - }, - { - name: 'subnetwork_name', - type: 'keyword', - description: 'Subnetwork on which the VM is operating.\n', - }, - ], - }, - { - name: 'source.instance', - type: 'group', - description: - 'If the source of the connection was a VM located on the same VPC, this field is populated with VM instance details. In a Shared VPC configuration, project_id corresponds to the project that owns the instance, usually the service project.\n', - fields: [ - { - name: 'project_id', - type: 'keyword', - description: 'ID of the project containing the VM.\n', - }, - { - name: 'region', - type: 'keyword', - description: 'Region of the VM.\n', - }, - { - name: 'zone', - type: 'keyword', - description: 'Zone of the VM.\n', - }, - ], - }, - { - name: 'source.vpc', - type: 'group', - description: - 'If the source of the connection was a VM located on the same VPC, this field is populated with VPC network details. In a Shared VPC configuration, project_id corresponds to that of the host project.\n', - fields: [ - { - name: 'project_id', - type: 'keyword', - description: 'ID of the project containing the VM.\n', - }, - { - name: 'vpc_name', - type: 'keyword', - description: 'VPC on which the VM is operating.\n', - }, - { - name: 'subnetwork_name', - type: 'keyword', - description: 'Subnetwork on which the VM is operating.\n', - }, - ], - }, - { - name: 'audit', - type: 'group', - description: 'Fields for Google Cloud audit logs.\n', - fields: [ - { - name: 'type', - type: 'keyword', - description: 'Type property.\n', - }, - { - name: 'authentication_info', - type: 'group', - description: 'Authentication information.\n', - fields: [ - { - name: 'principal_email', - type: 'keyword', - description: - 'The email address of the authenticated user making the request.\n', - }, - { - name: 'authority_selector', - type: 'keyword', - description: - 'The authority selector specified by the requestor, if any. It is not guaranteed that the principal was allowed to use this authority.\n', - }, - ], - }, - { - name: 'authorization_info', - type: 'array', - description: 'Authorization information for the operation.\n', - fields: [ - { - name: 'permission', - type: 'keyword', - description: 'The required IAM permission.\n', - }, - { - name: 'granted', - type: 'boolean', - description: - 'Whether or not authorization for resource and permission was granted.\n', - }, - { - name: 'resource_attributes', - type: 'group', - description: 'The attributes of the resource.\n', - fields: [ - { - name: 'service', - type: 'keyword', - description: 'The name of the service.\n', - }, - { - name: 'name', - type: 'keyword', - description: 'The name of the resource.\n', - }, - { - name: 'type', - type: 'keyword', - description: 'The type of the resource.\n', - }, - ], - }, - ], - }, - { - name: 'method_name', - type: 'keyword', - description: - "The name of the service method or operation. For API calls, this should be the name of the API method. For example, 'google.datastore.v1.Datastore.RunQuery'.\n", - }, - { - name: 'num_response_items', - type: 'long', - description: - 'The number of items returned from a List or Query API method, if applicable.\n', - }, - { - name: 'request', - type: 'group', - description: 'The operation request.\n', - fields: [ - { - name: 'proto_name', - type: 'keyword', - description: 'Type property of the request.\n', - }, - { - name: 'filter', - type: 'keyword', - description: 'Filter of the request.\n', - }, - { - name: 'name', - type: 'keyword', - description: 'Name of the request.\n', - }, - { - name: 'resource_name', - type: 'keyword', - description: 'Name of the request resource.\n', - }, - ], - }, - { - name: 'request_metadata', - type: 'group', - description: 'Metadata about the request.\n', - fields: [ - { - name: 'caller_ip', - type: 'ip', - description: 'The IP address of the caller.\n', - }, - { - name: 'caller_supplied_user_agent', - type: 'keyword', - description: - 'The user agent of the caller. This information is not authenticated and should be treated accordingly.\n', - }, - ], - }, - { - name: 'resource_name', - type: 'keyword', - description: - "The resource or collection that is the target of the operation. The name is a scheme-less URI, not including the API service name. For example, 'shelves/SHELF_ID/books'.\n", - }, - { - name: 'resource_location', - type: 'group', - description: 'The location of the resource.\n', - fields: [ - { - name: 'current_locations', - type: 'keyword', - description: 'Current locations of the resource.\n', - }, - ], - }, - { - name: 'service_name', - type: 'keyword', - description: - 'The name of the API service performing the operation. For example, datastore.googleapis.com.\n', - }, - { - name: 'status', - type: 'group', - description: 'The status of the overall operation.\n', - fields: [ - { - name: 'code', - type: 'integer', - description: - 'The status code, which should be an enum value of google.rpc.Code.\n', - }, - { - name: 'message', - type: 'keyword', - description: - 'A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the google.rpc.Status.details field, or localized by the client.\n', - }, - ], - }, - ], - }, - { - name: 'firewall', - type: 'group', - description: 'Fields for Google Cloud Firewall logs.\n', - fields: [ - { - name: 'rule_details', - type: 'group', - description: 'Description of the firewall rule that matched this connection.\n', - fields: [ - { - name: 'priority', - type: 'long', - description: 'The priority for the firewall rule.', - }, - { - name: 'action', - type: 'keyword', - description: 'Action that the rule performs on match.', - }, - { - name: 'direction', - type: 'keyword', - description: 'Direction of traffic that matches this rule.', - }, - { - name: 'reference', - type: 'keyword', - description: 'Reference to the firewall rule.', - }, - { - name: 'source_range', - type: 'keyword', - description: 'List of source ranges that the firewall rule applies to.', - }, - { - name: 'destination_range', - type: 'keyword', - description: 'List of destination ranges that the firewall applies to.', - }, - { - name: 'source_tag', - type: 'keyword', - description: 'List of all the source tags that the firewall rule applies to.\n', - }, - { - name: 'target_tag', - type: 'keyword', - description: 'List of all the target tags that the firewall rule applies to.\n', - }, - { - name: 'ip_port_info', - type: 'array', - description: 'List of ip protocols and applicable port ranges for rules.\n', - }, - { - name: 'source_service_account', - type: 'keyword', - description: - 'List of all the source service accounts that the firewall rule applies to.\n', - }, - { - name: 'target_service_account', - type: 'keyword', - description: - 'List of all the target service accounts that the firewall rule applies to.\n', - }, - ], - }, - ], - }, - { - name: 'vpcflow', - type: 'group', - description: 'Fields for Google Cloud VPC flow logs.\n', - fields: [ - { - name: 'reporter', - type: 'keyword', - description: "The side which reported the flow. Can be either 'SRC' or 'DEST'.\n", - }, - { - name: 'rtt.ms', - type: 'long', - description: - 'Latency as measured (for TCP flows only) during the time interval. This is the time elapsed between sending a SEQ and receiving a corresponding ACK and it contains the network RTT as well as the application related delay.\n', - }, - ], - }, - ], - }, - ], - }, - { - key: 'ibmmq', - title: 'ibmmq', - description: 'ibmmq Module\n', - release: 'ga', - fields: [ - { - name: 'ibmmq', - type: 'group', - description: '\n', - fields: [ - { - name: 'errorlog', - description: 'IBM MQ error logs', - type: 'group', - fields: [ - { - name: 'installation', - description: - 'This is the installation name which can be given at installation time.\nEach installation of IBM MQ on UNIX, Linux, and Windows, has a unique identifier known as an installation name. The installation name is used to associate things such as queue managers and configuration files with an installation.\n', - type: 'keyword', - }, - { - name: 'qmgr', - description: - 'Name of the queue manager. Queue managers provide queuing services to applications, and manages the queues that belong to them.\n', - type: 'keyword', - }, - { - name: 'arithinsert', - description: 'Changing content based on error.id', - type: 'keyword', - }, - { - name: 'commentinsert', - description: 'Changing content based on error.id', - type: 'keyword', - }, - { - name: 'errordescription', - description: 'Please add description', - example: 'Please add example', - type: 'text', - }, - { - name: 'explanation', - description: 'Explaines the error in more detail', - type: 'keyword', - }, - { - name: 'action', - description: 'Defines what to do when the error occurs', - type: 'keyword', - }, - { - name: 'code', - description: 'Error code.', - type: 'keyword', - }, - ], - }, - ], - }, - ], - }, - { - key: 'iptables', - title: 'iptables', - description: 'Module for handling the iptables logs.\n', - fields: [ - { - name: 'iptables', - type: 'group', - description: 'Fields from the iptables logs.\n', - fields: [ - { - name: 'ether_type', - type: 'long', - description: - 'Value of the ethernet type field identifying the network layer protocol.\n', - }, - { - name: 'flow_label', - type: 'integer', - description: 'IPv6 flow label.\n', - }, - { - name: 'fragment_flags', - type: 'keyword', - description: 'IP fragment flags. A combination of CE, DF and MF.\n', - }, - { - name: 'fragment_offset', - type: 'long', - description: 'Offset of the current IP fragment.\n', - }, - { - name: 'icmp', - type: 'group', - description: 'ICMP fields.\n', - fields: [ - { - name: 'code', - type: 'long', - description: 'ICMP code.\n', - }, - { - name: 'id', - type: 'long', - description: 'ICMP ID.\n', - }, - { - name: 'parameter', - type: 'long', - description: 'ICMP parameter.\n', - }, - { - name: 'redirect', - type: 'ip', - description: 'ICMP redirect address.\n', - }, - { - name: 'seq', - type: 'long', - description: 'ICMP sequence number.\n', - }, - { - name: 'type', - type: 'long', - description: 'ICMP type.\n', - }, - ], - }, - { - name: 'id', - type: 'long', - description: 'Packet identifier.\n', - }, - { - name: 'incomplete_bytes', - type: 'long', - description: 'Number of incomplete bytes.\n', - }, - { - name: 'input_device', - type: 'keyword', - description: 'Device that received the packet.\n', - }, - { - name: 'precedence_bits', - type: 'short', - description: 'IP precedence bits.\n', - }, - { - name: 'tos', - type: 'long', - description: 'IP Type of Service field.\n', - }, - { - name: 'length', - type: 'long', - description: 'Packet length.\n', - }, - { - name: 'output_device', - type: 'keyword', - description: 'Device that output the packet.\n', - }, - { - name: 'tcp', - type: 'group', - description: 'TCP fields.\n', - fields: [ - { - name: 'flags', - type: 'keyword', - description: 'TCP flags.\n', - }, - { - name: 'reserved_bits', - type: 'short', - description: 'TCP reserved bits.\n', - }, - { - name: 'seq', - type: 'long', - description: 'TCP sequence number.\n', - }, - { - name: 'ack', - type: 'long', - description: 'TCP Acknowledgment number.\n', - }, - { - name: 'window', - type: 'long', - description: 'Advertised TCP window size.\n', - }, - ], - }, - { - name: 'ttl', - type: 'integer', - description: 'Time To Live field.\n', - }, - { - name: 'udp', - type: 'group', - description: 'UDP fields.\n', - fields: [ - { - name: 'length', - type: 'long', - description: 'Length of the UDP header and payload.\n', - }, - ], - }, - { - name: 'ubiquiti', - type: 'group', - description: 'Fields for Ubiquiti network devices.\n', - fields: [ - { - name: 'input_zone', - type: 'keyword', - description: 'Input zone.\n', - }, - { - name: 'output_zone', - type: 'keyword', - description: 'Output zone.\n', - }, - { - name: 'rule_number', - type: 'keyword', - description: 'The rule number within the rule set.', - }, - { - name: 'rule_set', - type: 'keyword', - description: 'The rule set name.', - }, - ], - }, - ], - }, - ], - }, - { - key: 'misp', - title: 'MISP', - description: 'Module for handling threat information from MISP.\n', - fields: [ - { - name: 'misp', - type: 'group', - description: 'Fields from MISP threat information.\n', - fields: [ - { - name: 'attack_pattern', - title: 'Attack Pattern', - short: 'Fields that let you store attack patterns', - description: - 'Fields provide support for specifying information about attack patterns.\n', - type: 'group', - fields: [ - { - name: 'id', - level: 'core', - type: 'keyword', - description: 'Identifier of the threat indicator.\n', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - description: 'Name of the attack pattern.\n', - }, - { - name: 'description', - level: 'extended', - type: 'text', - description: 'Description of the attack pattern.\n', - }, - { - name: 'kill_chain_phases', - level: 'extended', - type: 'keyword', - description: 'The kill chain phase(s) to which this attack pattern corresponds.\n', - }, - ], - }, - { - name: 'campaign', - title: 'Campaign', - short: 'Fields that let you store campaign information', - description: 'Fields provide support for specifying information about campaigns.\n', - type: 'group', - fields: [ - { - name: 'id', - level: 'core', - type: 'keyword', - description: 'Identifier of the campaign.\n', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - description: 'Name of the campaign.\n', - }, - { - name: 'description', - level: 'extended', - type: 'text', - description: 'Description of the campaign.\n', - }, - { - name: 'aliases', - level: 'extended', - type: 'text', - description: 'Alternative names used to identify this campaign.\n', - }, - { - name: 'first_seen', - level: 'core', - type: 'date', - description: 'The time that this Campaign was first seen, in RFC3339 format.\n', - }, - { - name: 'last_seen', - level: 'core', - type: 'date', - description: 'The time that this Campaign was last seen, in RFC3339 format.\n', - }, - { - name: 'objective', - level: 'core', - type: 'keyword', - description: - "This field defines the Campaign's primary goal, objective, desired outcome, or intended effect.\n", - }, - ], - }, - { - name: 'course_of_action', - title: 'Course of Action', - short: 'Fields that let you store information about course of action.', - description: - 'A Course of Action is an action taken either to prevent an attack or to respond to an attack that is in progress.\n', - type: 'group', - fields: [ - { - name: 'id', - level: 'core', - type: 'keyword', - description: 'Identifier of the Course of Action.\n', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - description: 'The name used to identify the Course of Action.\n', - }, - { - name: 'description', - level: 'extended', - type: 'text', - description: 'Description of the Course of Action.\n', - }, - ], - }, - { - name: 'identity', - title: 'Identity', - short: 'Fields that let you store information about identity.', - description: - 'Identity can represent actual individuals, organizations, or groups, as well as classes of individuals, organizations, or groups.\n', - type: 'group', - fields: [ - { - name: 'id', - level: 'core', - type: 'keyword', - description: 'Identifier of the Identity.\n', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - description: 'The name used to identify the Identity.\n', - }, - { - name: 'description', - level: 'extended', - type: 'text', - description: 'Description of the Identity.\n', - }, - { - name: 'identity_class', - level: 'core', - type: 'keyword', - description: - 'The type of entity that this Identity describes, e.g., an individual or organization. Open Vocab - identity-class-ov\n', - }, - { - name: 'labels', - level: 'extended', - type: 'keyword', - description: 'The list of roles that this Identity performs.\n', - example: 'CEO\n', - }, - { - name: 'sectors', - level: 'extended', - type: 'keyword', - description: - 'The list of sectors that this Identity belongs to. Open Vocab - industry-sector-ov\n', - }, - { - name: 'contact_information', - level: 'extended', - type: 'text', - description: - 'The contact information (e-mail, phone number, etc.) for this Identity.\n', - }, - ], - }, - { - name: 'intrusion_set', - title: 'Intrusion Set', - short: 'Fields that let you store information about Intrusion Set.', - description: - 'An Intrusion Set is a grouped set of adversary behavior and resources with common properties that is believed to be orchestrated by a single organization.\n', - type: 'group', - fields: [ - { - name: 'id', - level: 'core', - type: 'keyword', - description: 'Identifier of the Intrusion Set.\n', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - description: 'The name used to identify the Intrusion Set.\n', - }, - { - name: 'description', - level: 'extended', - type: 'text', - description: 'Description of the Intrusion Set.\n', - }, - { - name: 'aliases', - level: 'extended', - type: 'text', - description: 'Alternative names used to identify the Intrusion Set.\n', - }, - { - name: 'first_seen', - level: 'extended', - type: 'date', - description: - 'The time that this Intrusion Set was first seen, in RFC3339 format.\n', - }, - { - name: 'last_seen', - level: 'extended', - type: 'date', - description: 'The time that this Intrusion Set was last seen, in RFC3339 format.\n', - }, - { - name: 'goals', - level: 'extended', - type: 'text', - description: - 'The high level goals of this Intrusion Set, namely, what are they trying to do.\n', - }, - { - name: 'resource_level', - level: 'extended', - type: 'text', - description: - 'This defines the organizational level at which this Intrusion Set typically works. Open Vocab - attack-resource-level-ov\n', - }, - { - name: 'primary_motivation', - level: 'extended', - type: 'text', - description: - 'The primary reason, motivation, or purpose behind this Intrusion Set. Open Vocab - attack-motivation-ov\n', - }, - { - name: 'secondary_motivations', - level: 'extended', - type: 'text', - description: - 'The secondary reasons, motivations, or purposes behind this Intrusion Set. Open Vocab - attack-motivation-ov\n', - }, - ], - }, - { - name: 'malware', - title: 'Malware', - short: 'Fields that let you store information about Malware.', - description: - "Malware is a type of TTP that is also known as malicious code and malicious software, refers to a program that is inserted into a system, usually covertly, with the intent of compromising the confidentiality, integrity, or availability of the victim's data, applications, or operating system (OS) or of otherwise annoying or disrupting the victim.\n", - type: 'group', - fields: [ - { - name: 'id', - level: 'core', - type: 'keyword', - description: 'Identifier of the Malware.\n', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - description: 'The name used to identify the Malware.\n', - }, - { - name: 'description', - level: 'extended', - type: 'text', - description: 'Description of the Malware.\n', - }, - { - name: 'labels', - level: 'core', - type: 'keyword', - description: - 'The type of malware being described. Open Vocab - malware-label-ov. adware,backdoor,bot,ddos,dropper,exploit-kit,keylogger,ransomware, remote-access-trojan,resource-exploitation,rogue-security-software,rootkit, screen-capture,spyware,trojan,virus,worm\n', - }, - { - name: 'kill_chain_phases', - format: 'string', - level: 'extended', - type: 'keyword', - description: - 'The list of kill chain phases for which this Malware instance can be used.\n', - }, - ], - }, - { - name: 'note', - title: 'Note', - short: 'Fields that let you store information about Malware.', - description: - 'A Note is a comment or note containing informative text to help explain the context of one or more STIX Objects (SDOs or SROs) or to provide additional analysis that is not contained in the original object.\n', - type: 'group', - fields: [ - { - name: 'id', - level: 'core', - type: 'keyword', - description: 'Identifier of the Note.\n', - }, - { - name: 'summary', - level: 'extended', - type: 'keyword', - description: 'A brief description used as a summary of the Note.\n', - }, - { - name: 'description', - level: 'extended', - type: 'text', - description: 'The content of the Note.\n', - }, - { - name: 'authors', - level: 'extended', - type: 'keyword', - description: 'The name of the author(s) of this Note.\n', - }, - { - name: 'object_refs', - level: 'extended', - type: 'keyword', - description: - 'The STIX Objects (SDOs and SROs) that the note is being applied to.\n', - }, - ], - }, - { - name: 'threat_indicator', - title: 'Threat Indicator', - short: 'Fields that let you store Threat Indicators', - description: - 'Fields provide support for specifying information about threat indicators, and related matching patterns.\n', - type: 'group', - fields: [ - { - name: 'labels', - level: 'core', - type: 'keyword', - description: 'list of type open-vocab that specifies the type of indicator.\n', - example: 'Domain Watchlist\n', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - description: 'Identifier of the threat indicator.\n', - }, - { - name: 'version', - level: 'core', - type: 'keyword', - description: 'Version of the threat indicator.\n', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - description: 'Type of the threat indicator.\n', - }, - { - name: 'description', - level: 'core', - type: 'text', - description: 'Description of the threat indicator.\n', - }, - { - name: 'feed', - level: 'core', - type: 'text', - description: 'Name of the threat feed.\n', - }, - { - name: 'valid_from', - level: 'core', - type: 'date', - description: - 'The time from which this Indicator should be considered valuable intelligence, in RFC3339 format.\n', - }, - { - name: 'valid_until', - level: 'core', - type: 'date', - description: - 'The time at which this Indicator should no longer be considered valuable intelligence. If the valid_until property is omitted, then there is no constraint on the latest time for which the indicator should be used, in RFC3339 format.\n', - }, - { - name: 'severity', - format: 'string', - level: 'core', - type: 'keyword', - description: 'Threat severity to which this indicator corresponds.\n', - example: 'high', - }, - { - name: 'confidence', - level: 'core', - type: 'keyword', - description: 'Confidence level to which this indicator corresponds.\n', - example: 'high', - }, - { - name: 'kill_chain_phases', - format: 'string', - level: 'extended', - type: 'keyword', - description: 'The kill chain phase(s) to which this indicator corresponds.\n', - }, - { - name: 'mitre_tactic', - format: 'string', - level: 'extended', - type: 'keyword', - description: 'MITRE tactics to which this indicator corresponds.\n', - example: 'Initial Access', - }, - { - name: 'mitre_technique', - format: 'string', - level: 'extended', - type: 'keyword', - description: 'MITRE techniques to which this indicator corresponds.\n', - example: 'Drive-by Compromise', - }, - { - name: 'attack_pattern', - level: 'core', - type: 'keyword', - description: - 'The attack_pattern for this indicator is a STIX Pattern as specified in STIX Version 2.0 Part 5 - STIX Patterning.\n', - example: "[destination:ip = '91.219.29.188/32']\n", - }, - { - name: 'attack_pattern_kql', - level: 'core', - type: 'keyword', - description: - 'The attack_pattern for this indicator is KQL query that matches the attack_pattern specified in the STIX Pattern format.\n', - example: 'destination.ip: "91.219.29.188/32"\n', - }, - { - name: 'negate', - level: 'core', - type: 'boolean', - description: 'When set to true, it specifies the absence of the attack_pattern.\n', - }, - { - name: 'intrusion_set', - level: 'extended', - type: 'keyword', - description: 'Name of the intrusion set if known.\n', - }, - { - name: 'campaign', - level: 'extended', - type: 'keyword', - description: 'Name of the attack campaign if known.\n', - }, - { - name: 'threat_actor', - level: 'extended', - type: 'keyword', - description: 'Name of the threat actor if known.\n', - }, - ], - }, - { - name: 'observed_data', - title: 'Observed Data', - short: 'Fields that let you store information about Observed Data.', - description: - 'Observed data conveys information that was observed on systems and networks, such as log data or network traffic, using the Cyber Observable specification.\n', - type: 'group', - fields: [ - { - name: 'id', - level: 'core', - type: 'keyword', - description: 'Identifier of the Observed Data.\n', - }, - { - name: 'first_observed', - level: 'core', - type: 'date', - description: - 'The beginning of the time window that the data was observed, in RFC3339 format.\n', - }, - { - name: 'last_observed', - level: 'core', - type: 'date', - description: - 'The end of the time window that the data was observed, in RFC3339 format.\n', - }, - { - name: 'number_observed', - level: 'core', - type: 'integer', - description: - 'The number of times the data represented in the objects property was observed. This MUST be an integer between 1 and 999,999,999 inclusive.\n', - }, - { - name: 'objects', - level: 'core', - type: 'keyword', - description: - 'A dictionary of Cyber Observable Objects that describes the single fact that was observed.\n', - }, - ], - }, - { - name: 'report', - title: 'Report', - short: 'Fields that let you store information about Report.', - description: - 'Reports are collections of threat intelligence focused on one or more topics, such as a description of a threat actor, malware, or attack technique, including context and related details.\n', - type: 'group', - fields: [ - { - name: 'id', - level: 'core', - type: 'keyword', - description: 'Identifier of the Report.\n', - }, - { - name: 'labels', - level: 'core', - type: 'keyword', - description: - 'This field is an Open Vocabulary that specifies the primary subject of this report. Open Vocab - report-label-ov. threat-report,attack-pattern,campaign,identity,indicator,malware,observed-data,threat-actor,tool,vulnerability\n', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - description: 'The name used to identify the Report.\n', - }, - { - name: 'description', - level: 'extended', - type: 'text', - description: 'A description that provides more details and context about Report.\n', - }, - { - name: 'published', - level: 'extended', - type: 'date', - description: - 'The date that this report object was officially published by the creator of this report, in RFC3339 format.\n', - }, - { - name: 'object_refs', - level: 'core', - type: 'text', - description: 'Specifies the STIX Objects that are referred to by this Report.\n', - }, - ], - }, - { - name: 'threat_actor', - title: 'Threat Actor', - short: 'Fields that let you store information about Threat Actor.', - description: - 'Threat Actors are actual individuals, groups, or organizations believed to be operating with malicious intent.\n', - type: 'group', - fields: [ - { - name: 'id', - level: 'core', - type: 'keyword', - description: 'Identifier of the Threat Actor.\n', - }, - { - name: 'labels', - level: 'core', - type: 'keyword', - description: - 'This field specifies the type of threat actor. Open Vocab - threat-actor-label-ov. activist,competitor,crime-syndicate,criminal,hacker,insider-accidental,insider-disgruntled,nation-state,sensationalist,spy,terrorist\n', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - description: 'The name used to identify this Threat Actor or Threat Actor group.\n', - }, - { - name: 'description', - level: 'extended', - type: 'text', - description: - 'A description that provides more details and context about the Threat Actor.\n', - }, - { - name: 'aliases', - level: 'extended', - type: 'text', - description: 'A list of other names that this Threat Actor is believed to use.\n', - }, - { - name: 'roles', - level: 'extended', - type: 'text', - description: - 'This is a list of roles the Threat Actor plays. Open Vocab - threat-actor-role-ov. agent,director,independent,sponsor,infrastructure-operator,infrastructure-architect,malware-author\n', - }, - { - name: 'goals', - level: 'extended', - type: 'text', - description: - 'The high level goals of this Threat Actor, namely, what are they trying to do.\n', - }, - { - name: 'sophistication', - level: 'extended', - type: 'text', - description: - 'The skill, specific knowledge, special training, or expertise a Threat Actor must have to perform the attack. Open Vocab - threat-actor-sophistication-ov. none,minimal,intermediate,advanced,strategic,expert,innovator\n', - }, - { - name: 'resource_level', - level: 'extended', - type: 'text', - description: - 'This defines the organizational level at which this Threat Actor typically works. Open Vocab - attack-resource-level-ov. individual,club,contest,team,organization,government\n', - }, - { - name: 'primary_motivation', - level: 'extended', - type: 'text', - description: - 'The primary reason, motivation, or purpose behind this Threat Actor. Open Vocab - attack-motivation-ov. accidental,coercion,dominance,ideology,notoriety,organizational-gain,personal-gain,personal-satisfaction,revenge,unpredictable\n', - }, - { - name: 'secondary_motivations', - level: 'extended', - type: 'text', - description: - 'The secondary reasons, motivations, or purposes behind this Threat Actor. Open Vocab - attack-motivation-ov. accidental,coercion,dominance,ideology,notoriety,organizational-gain,personal-gain,personal-satisfaction,revenge,unpredictable\n', - }, - { - name: 'personal_motivations', - level: 'extended', - type: 'text', - description: - 'The personal reasons, motivations, or purposes of the Threat Actor regardless of organizational goals. Open Vocab - attack-motivation-ov. accidental,coercion,dominance,ideology,notoriety,organizational-gain,personal-gain,personal-satisfaction,revenge,unpredictable\n', - }, - ], - }, - { - name: 'tool', - title: 'Tool', - short: 'Fields that let you store information about Tool.', - description: - 'Tools are legitimate software that can be used by threat actors to perform attacks.\n', - type: 'group', - fields: [ - { - name: 'id', - level: 'core', - type: 'keyword', - description: 'Identifier of the Tool.\n', - }, - { - name: 'labels', - level: 'core', - type: 'keyword', - description: - 'The kind(s) of tool(s) being described. Open Vocab - tool-label-ov. denial-of-service,exploitation,information-gathering,network-capture,credential-exploitation,remote-access,vulnerability-scanning\n', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - description: 'The name used to identify the Tool.\n', - }, - { - name: 'description', - level: 'extended', - type: 'text', - description: - 'A description that provides more details and context about the Tool.\n', - }, - { - name: 'tool_version', - level: 'extended', - type: 'keyword', - description: 'The version identifier associated with the Tool.\n', - }, - { - name: 'kill_chain_phases', - level: 'extended', - type: 'text', - description: - 'The list of kill chain phases for which this Tool instance can be used.\n', - }, - ], - }, - { - name: 'vulnerability', - title: 'Vulnerability', - short: 'Fields that let you store information about Vulnerability.', - description: - 'A Vulnerability is a mistake in software that can be directly used by a hacker to gain access to a system or network.\n', - type: 'group', - fields: [ - { - name: 'id', - level: 'core', - type: 'keyword', - description: 'Identifier of the Vulnerability.\n', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - description: 'The name used to identify the Vulnerability.\n', - }, - { - name: 'description', - level: 'extended', - type: 'text', - description: - 'A description that provides more details and context about the Vulnerability.\n', - }, - ], - }, - ], - }, - ], - }, - { - key: 'mssql', - title: 'mssql', - description: 'MS SQL Filebeat Module', - fields: [ - { - name: 'mssql', - type: 'group', - description: 'Fields from the MSSQL log files', - fields: [ - { - name: 'log', - description: 'Common log fields', - type: 'group', - fields: [ - { - name: 'origin', - description: - 'Origin of the message, usually the server but it can also be a recovery process', - type: 'keyword', - }, - ], - }, - ], - }, - ], - }, - { - key: 'netflow-module', - title: 'NetFlow', - description: - 'Module for receiving NetFlow and IPFIX flow records over UDP. The module does not add fields beyond what the netflow input provides.\n', - fields: [], - }, - { - key: 'o365', - title: 'Office 365', - description: 'Module for handling logs from Office 365.\n', - fields: [ - { - name: 'o365.audit', - type: 'group', - default_field: false, - description: 'Fields from Office 365 Management API audit logs.\n', - fields: [ - { - name: 'Actor', - type: 'array', - fields: [ - { - name: 'ID', - type: 'keyword', - }, - { - name: 'Type', - type: 'keyword', - }, - ], - }, - { - name: 'ActorContextId', - type: 'keyword', - }, - { - name: 'ActorIpAddress', - type: 'keyword', - }, - { - name: 'ActorUserId', - type: 'keyword', - }, - { - name: 'ActorYammerUserId', - type: 'keyword', - }, - { - name: 'AlertEntityId', - type: 'keyword', - }, - { - name: 'AlertId', - type: 'keyword', - }, - { - name: 'AlertLinks', - type: 'array', - }, - { - name: 'AlertType', - type: 'keyword', - }, - { - name: 'AppId', - type: 'keyword', - }, - { - name: 'ApplicationDisplayName', - type: 'keyword', - }, - { - name: 'ApplicationId', - type: 'keyword', - }, - { - name: 'AzureActiveDirectoryEventType', - type: 'keyword', - }, - { - name: 'ExchangeMetaData.*', - type: 'object', - }, - { - name: 'Category', - type: 'keyword', - }, - { - name: 'ClientAppId', - type: 'keyword', - }, - { - name: 'ClientInfoString', - type: 'keyword', - }, - { - name: 'ClientIP', - type: 'keyword', - }, - { - name: 'ClientIPAddress', - type: 'keyword', - }, - { - name: 'Comments', - type: 'text', - norms: false, - }, - { - name: 'CorrelationId', - type: 'keyword', - }, - { - name: 'CreationTime', - type: 'keyword', - }, - { - name: 'CustomUniqueId', - type: 'keyword', - }, - { - name: 'Data', - type: 'keyword', - }, - { - name: 'DataType', - type: 'keyword', - }, - { - name: 'EntityType', - type: 'keyword', - }, - { - name: 'EventData', - type: 'keyword', - }, - { - name: 'EventSource', - type: 'keyword', - }, - { - name: 'ExceptionInfo.*', - type: 'object', - }, - { - name: 'ExtendedProperties.*', - type: 'object', - }, - { - name: 'ExternalAccess', - type: 'keyword', - }, - { - name: 'GroupName', - type: 'keyword', - }, - { - name: 'Id', - type: 'keyword', - }, - { - name: 'ImplicitShare', - type: 'keyword', - }, - { - name: 'IncidentId', - type: 'keyword', - }, - { - name: 'InternalLogonType', - type: 'keyword', - }, - { - name: 'InterSystemsId', - type: 'keyword', - }, - { - name: 'IntraSystemId', - type: 'keyword', - }, - { - name: 'Item.*', - type: 'object', - }, - { - name: 'Item.*.*', - type: 'object', - }, - { - name: 'ItemName', - type: 'keyword', - }, - { - name: 'ItemType', - type: 'keyword', - }, - { - name: 'ListId', - type: 'keyword', - }, - { - name: 'ListItemUniqueId', - type: 'keyword', - }, - { - name: 'LogonError', - type: 'keyword', - }, - { - name: 'LogonType', - type: 'keyword', - }, - { - name: 'LogonUserSid', - type: 'keyword', - }, - { - name: 'MailboxGuid', - type: 'keyword', - }, - { - name: 'MailboxOwnerMasterAccountSid', - type: 'keyword', - }, - { - name: 'MailboxOwnerSid', - type: 'keyword', - }, - { - name: 'MailboxOwnerUPN', - type: 'keyword', - }, - { - name: 'Members', - type: 'array', - }, - { - name: 'Members.*', - type: 'object', - }, - { - name: 'ModifiedProperties.*.*', - type: 'object', - }, - { - name: 'Name', - type: 'keyword', - }, - { - name: 'ObjectId', - type: 'keyword', - }, - { - name: 'Operation', - type: 'keyword', - }, - { - name: 'OrganizationId', - type: 'keyword', - }, - { - name: 'OrganizationName', - type: 'keyword', - }, - { - name: 'OriginatingServer', - type: 'keyword', - }, - { - name: 'Parameters.*', - type: 'object', - }, - { - name: 'PolicyDetails', - type: 'array', - }, - { - name: 'PolicyId', - type: 'keyword', - }, - { - name: 'RecordType', - type: 'keyword', - }, - { - name: 'ResultStatus', - type: 'keyword', - }, - { - name: 'SensitiveInfoDetectionIsIncluded', - type: 'keyword', - }, - { - name: 'SharePointMetaData.*', - type: 'object', - }, - { - name: 'SessionId', - type: 'keyword', - }, - { - name: 'Severity', - type: 'keyword', - }, - { - name: 'Site', - type: 'keyword', - }, - { - name: 'SiteUrl', - type: 'keyword', - }, - { - name: 'Source', - type: 'keyword', - }, - { - name: 'SourceFileExtension', - type: 'keyword', - }, - { - name: 'SourceFileName', - type: 'keyword', - }, - { - name: 'SourceRelativeUrl', - type: 'keyword', - }, - { - name: 'Status', - type: 'keyword', - }, - { - name: 'SupportTicketId', - type: 'keyword', - }, - { - name: 'Target', - type: 'array', - fields: [ - { - name: 'ID', - type: 'keyword', - }, - { - name: 'Type', - type: 'keyword', - }, - ], - }, - { - name: 'TargetContextId', - type: 'keyword', - }, - { - name: 'TargetUserOrGroupName', - type: 'keyword', - }, - { - name: 'TargetUserOrGroupType', - type: 'keyword', - }, - { - name: 'TeamName', - type: 'keyword', - }, - { - name: 'TeamGuid', - type: 'keyword', - }, - { - name: 'UniqueSharingId', - type: 'keyword', - }, - { - name: 'UserAgent', - type: 'keyword', - }, - { - name: 'UserId', - type: 'keyword', - }, - { - name: 'UserKey', - type: 'keyword', - }, - { - name: 'UserType', - type: 'keyword', - }, - { - name: 'Version', - type: 'keyword', - }, - { - name: 'WebId', - type: 'keyword', - }, - { - name: 'Workload', - type: 'keyword', - }, - { - name: 'YammerNetworkId', - type: 'keyword', - }, - ], - }, - ], - }, - { - key: 'okta', - title: 'Okta', - description: 'Module for handling system logs from Okta.\n', - fields: [ - { - name: 'okta', - type: 'group', - default_field: false, - description: 'Fields from Okta.\n', - fields: [ - { - name: 'uuid', - title: 'UUID', - short: 'The unique identifier of the Okta LogEvent.', - description: 'The unique identifier of the Okta LogEvent.\n', - type: 'keyword', - }, - { - name: 'event_type', - title: 'Event Type', - short: 'The type of the LogEvent.', - description: 'The type of the LogEvent.\n', - type: 'keyword', - }, - { - name: 'version', - title: 'Version', - short: 'The version of the LogEvent.', - description: 'The version of the LogEvent.\n', - type: 'keyword', - }, - { - name: 'severity', - title: 'Severity', - short: 'The severity of the LogEvent.', - description: - 'The severity of the LogEvent. Must be one of DEBUG, INFO, WARN, or ERROR.\n', - type: 'keyword', - }, - { - name: 'display_message', - title: 'Display Message', - short: 'The display message of the LogEvent.', - description: 'The display message of the LogEvent.\n', - type: 'keyword', - }, - { - name: 'actor', - title: 'Actor', - short: 'Fields of the actor for the LogEvent.', - description: 'Fields that let you store information of the actor for the LogEvent.\n', - type: 'group', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'Identifier of the actor.\n', - }, - { - name: 'type', - type: 'keyword', - description: 'Type of the actor.\n', - }, - { - name: 'alternate_id', - type: 'keyword', - description: 'Alternate identifier of the actor.\n', - }, - { - name: 'display_name', - type: 'keyword', - description: 'Display name of the actor.\n', - }, - ], - }, - { - name: 'client', - title: 'Client', - short: 'Fields about the client of the actor.', - description: 'Fields that let you store information about the client of the actor.\n', - type: 'group', - fields: [ - { - name: 'ip', - type: 'ip', - description: 'The IP address of the client.\n', - }, - { - name: 'user_agent', - description: 'Fields about the user agent information of the client.\n', - type: 'group', - fields: [ - { - name: 'raw_user_agent', - type: 'keyword', - description: 'The raw informaton of the user agent.\n', - }, - { - name: 'os', - type: 'keyword', - description: 'The OS informaton.\n', - }, - { - name: 'browser', - type: 'keyword', - description: 'The browser informaton of the client.\n', - }, - ], - }, - { - name: 'zone', - type: 'keyword', - description: 'The zone information of the client.\n', - }, - { - name: 'device', - type: 'keyword', - description: 'The information of the client device.\n', - }, - { - name: 'id', - type: 'keyword', - description: 'The identifier of the client.\n', - }, - ], - }, - { - name: 'outcome', - title: 'Outcome of the LogEvent.', - short: 'Fields that let you store information about the outcome.', - description: 'Fields that let you store information about the outcome.\n', - type: 'group', - fields: [ - { - name: 'reason', - type: 'keyword', - description: 'The reason of the outcome.\n', - }, - { - name: 'result', - type: 'keyword', - description: - 'The result of the outcome. Must be one of: SUCCESS, FAILURE, SKIPPED, ALLOW, DENY, CHALLENGE, UNKNOWN.\n', - }, - ], - }, - { - name: 'target', - title: 'Target', - short: 'The list of targets.', - description: 'The list of targets.\n', - type: 'array', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'Identifier of the actor.\n', - }, - { - name: 'type', - type: 'keyword', - description: 'Type of the actor.\n', - }, - { - name: 'alternate_id', - type: 'keyword', - description: 'Alternate identifier of the actor.\n', - }, - { - name: 'display_name', - type: 'keyword', - description: 'Display name of the actor.\n', - }, - ], - }, - { - name: 'transaction', - title: 'Transaction', - short: 'Fields that let you store information about related transaction.', - description: 'Fields that let you store information about related transaction.\n', - type: 'group', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'Identifier of the transaction.\n', - }, - { - name: 'type', - type: 'keyword', - description: 'The type of transaction. Must be one of "WEB", "JOB".\n', - }, - ], - }, - { - name: 'debug_context', - title: 'Debug Context', - short: 'Fields that let you store information about the debug context.', - description: 'Fields that let you store information about the debug context.\n', - type: 'group', - fields: [ - { - name: 'debug_data', - description: 'The debug data.\n', - type: 'group', - fields: [ - { - name: 'device_fingerprint', - type: 'keyword', - description: 'The fingerprint of the device.\n', - }, - { - name: 'request_id', - type: 'keyword', - description: 'The identifier of the request.\n', - }, - { - name: 'request_uri', - type: 'keyword', - description: 'The request URI.\n', - }, - { - name: 'threat_suspected', - type: 'keyword', - description: 'Threat suspected.\n', - }, - { - name: 'url', - type: 'keyword', - description: 'The URL.\n', - }, - ], - }, - ], - }, - { - name: 'authentication_context', - title: 'Authentication Context', - short: 'Fields that let you store information about authentication context.', - description: 'Fields that let you store information about authentication context.\n', - type: 'group', - fields: [ - { - name: 'authentication_provider', - type: 'keyword', - description: - 'The information about the authentication provider. Must be one of OKTA_AUTHENTICATION_PROVIDER, ACTIVE_DIRECTORY, LDAP, FEDERATION, SOCIAL, FACTOR_PROVIDER.\n', - }, - { - name: 'authentication_step', - type: 'integer', - description: 'The authentication step.\n', - }, - { - name: 'credential_provider', - type: 'keyword', - description: - 'The information about credential provider. Must be one of OKTA_CREDENTIAL_PROVIDER, RSA, SYMANTEC, GOOGLE, DUO, YUBIKEY.\n', - }, - { - name: 'credential_type', - type: 'keyword', - description: - 'The information about credential type. Must be one of OTP, SMS, PASSWORD, ASSERTION, IWA, EMAIL, OAUTH2, JWT, CERTIFICATE, PRE_SHARED_SYMMETRIC_KEY, OKTA_CLIENT_SESSION, DEVICE_UDID.\n', - }, - { - name: 'issuer', - description: 'The information about the issuer.\n', - type: 'array', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'The identifier of the issuer.\n', - }, - { - name: 'type', - type: 'keyword', - description: 'The type of the issuer.\n', - }, - ], - }, - { - name: 'external_session_id', - type: 'keyword', - description: 'The session identifer of the external session if any.\n', - }, - { - name: 'interface', - type: 'keyword', - description: 'The interface used. e.g., Outlook, Office365, wsTrust\n', - }, - ], - }, - { - name: 'security_context', - title: 'Security Context', - short: 'Fields that let you store information about security context.', - description: 'Fields that let you store information about security context.\n', - type: 'group', - fields: [ - { - name: 'as', - type: 'group', - description: 'The autonomous system.\n', - fields: [ - { - name: 'number', - type: 'integer', - description: 'The AS number.\n', - }, - { - name: 'organization', - type: 'group', - description: 'The organization that owns the AS number.\n', - fields: [ - { - name: 'name', - type: 'keyword', - description: 'The organization name.\n', - }, - ], - }, - ], - }, - { - name: 'isp', - type: 'keyword', - description: 'The Internet Service Provider.\n', - }, - { - name: 'domain', - type: 'keyword', - description: 'The domain name.\n', - }, - { - name: 'is_proxy', - type: 'boolean', - description: 'Whether it is a proxy or not.\n', - }, - ], - }, - { - name: 'request', - title: 'Request', - short: 'Fields that let you store information about the request.', - description: - 'Fields that let you store information about the request, in the form of list of ip_chain.\n', - type: 'group', - fields: [ - { - name: 'ip_chain', - description: 'List of ip_chain objects.\n', - type: 'group', - fields: [ - { - name: 'ip', - type: 'ip', - description: 'IP address.\n', - }, - { - name: 'version', - type: 'keyword', - description: 'IP version. Must be one of V4, V6.\n', - }, - { - name: 'source', - type: 'keyword', - description: 'Source information.\n', - }, - { - name: 'geographical_context', - description: 'Geographical information.\n', - type: 'group', - fields: [ - { - name: 'city', - type: 'keyword', - description: 'The city.', - }, - { - name: 'state', - type: 'keyword', - description: 'The state.', - }, - { - name: 'postal_code', - type: 'keyword', - description: 'The postal code.', - }, - { - name: 'country', - type: 'keyword', - description: 'The country.', - }, - { - name: 'geolocation', - description: 'Geolocation information.\n', - type: 'geo_point', - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], - }, - { - key: 'panw', - title: 'panw', - description: 'Module for Palo Alto Networks (PAN-OS)\n', - fields: [ - { - name: 'panw', - type: 'group', - description: 'Fields from the panw module.\n', - fields: [ - { - name: 'panos', - type: 'group', - description: 'Fields for the Palo Alto Networks PAN-OS logs.\n', - fields: [ - { - name: 'ruleset', - type: 'keyword', - description: 'Name of the rule that matched this session.\n', - }, - { - name: 'source', - type: 'group', - description: 'Fields to extend the top-level source object.\n', - fields: [ - { - name: 'zone', - type: 'keyword', - description: 'Source zone for this session.\n', - }, - { - name: 'interface', - type: 'keyword', - description: 'Source interface for this session.\n', - }, - { - name: 'nat', - type: 'group', - description: 'Post-NAT source address, if source NAT is performed.\n', - fields: [ - { - name: 'ip', - type: 'ip', - description: 'Post-NAT source IP.\n', - }, - { - name: 'port', - type: 'long', - description: 'Post-NAT source port.\n', - }, - ], - }, - ], - }, - { - name: 'destination', - type: 'group', - description: 'Fields to extend the top-level destination object.\n', - fields: [ - { - name: 'zone', - type: 'keyword', - description: 'Destination zone for this session.\n', - }, - { - name: 'interface', - type: 'keyword', - description: 'Destination interface for this session.\n', - }, - { - name: 'nat', - type: 'group', - description: 'Post-NAT destination address, if destination NAT is performed.\n', - fields: [ - { - name: 'ip', - type: 'ip', - description: 'Post-NAT destination IP.\n', - }, - { - name: 'port', - type: 'long', - description: 'Post-NAT destination port.\n', - }, - ], - }, - ], - }, - { - name: 'network', - type: 'group', - description: 'Fields to extend the top-level network object.\n', - fields: [ - { - name: 'pcap_id', - type: 'keyword', - description: 'Packet capture ID for a threat.\n', - }, - { - name: 'nat', - type: 'group', - fields: [ - { - name: 'community_id', - type: 'keyword', - description: 'Community ID flow-hash for the NAT 5-tuple.\n', - }, - ], - }, - ], - }, - { - name: 'file', - type: 'group', - description: 'Fields to extend the top-level file object.\n', - fields: [ - { - name: 'hash', - description: - 'Binary hash for a threat file sent to be analyzed by the WildFire service.\n', - type: 'keyword', - }, - ], - }, - { - name: 'url', - type: 'group', - description: 'Fields to extend the top-level url object.\n', - fields: [ - { - name: 'category', - type: 'keyword', - description: - "For threat URLs, it's the URL category. For WildFire, the verdict on the file and is either 'malicious', 'grayware', or 'benign'.\n", - }, - ], - }, - { - name: 'flow_id', - type: 'keyword', - description: 'Internal numeric identifier for each session.\n', - }, - { - name: 'sequence_number', - type: 'long', - description: - 'Log entry identifier that is incremented sequentially. Unique for each log type.\n', - }, - { - name: 'threat.resource', - type: 'keyword', - description: 'URL or file name for a threat.\n', - }, - { - name: 'threat.id', - type: 'keyword', - description: 'Palo Alto Networks identifier for the threat.\n', - }, - { - name: 'threat.name', - type: 'keyword', - description: 'Palo Alto Networks name for the threat.\n', - }, - ], - }, - ], - }, - ], - }, - { - key: 'rabbitmq', - title: 'RabbitMQ', - description: 'RabbitMQ Module\n', - fields: [ - { - name: 'rabbitmq', - type: 'group', - description: '\n', - fields: [ - { - name: 'log', - type: 'group', - description: 'RabbitMQ log files\n', - fields: [ - { - name: 'pid', - type: 'keyword', - description: 'The Erlang process id', - example: '<0.222.0>', - }, - ], - }, - ], - }, - ], - }, - { - key: 'suricata', - title: 'Suricata', - description: 'Module for handling the EVE JSON logs produced by Suricata.\n', - fields: [ - { - name: 'suricata', - type: 'group', - description: 'Fields from the Suricata EVE log file.\n', - fields: [ - { - name: 'eve', - type: 'group', - description: 'Fields exported by the EVE JSON logs\n', - fields: [ - { - name: 'event_type', - type: 'keyword', - }, - { - name: 'app_proto_orig', - type: 'keyword', - }, - { - name: 'tcp', - type: 'group', - fields: [ - { - name: 'tcp_flags', - type: 'keyword', - }, - { - name: 'psh', - type: 'boolean', - }, - { - name: 'tcp_flags_tc', - type: 'keyword', - }, - { - name: 'ack', - type: 'boolean', - }, - { - name: 'syn', - type: 'boolean', - }, - { - name: 'state', - type: 'keyword', - }, - { - name: 'tcp_flags_ts', - type: 'keyword', - }, - { - name: 'rst', - type: 'boolean', - }, - { - name: 'fin', - type: 'boolean', - }, - ], - }, - { - name: 'fileinfo', - type: 'group', - fields: [ - { - name: 'sha1', - type: 'keyword', - }, - { - name: 'filename', - type: 'alias', - path: 'file.path', - }, - { - name: 'tx_id', - type: 'long', - }, - { - name: 'state', - type: 'keyword', - }, - { - name: 'stored', - type: 'boolean', - }, - { - name: 'gaps', - type: 'boolean', - }, - { - name: 'sha256', - type: 'keyword', - }, - { - name: 'md5', - type: 'keyword', - }, - { - name: 'size', - type: 'alias', - path: 'file.size', - }, - ], - }, - { - name: 'icmp_type', - type: 'long', - }, - { - name: 'dest_port', - type: 'alias', - path: 'destination.port', - }, - { - name: 'src_port', - type: 'alias', - path: 'source.port', - }, - { - name: 'proto', - type: 'alias', - path: 'network.transport', - }, - { - name: 'pcap_cnt', - type: 'long', - }, - { - name: 'src_ip', - type: 'alias', - path: 'source.ip', - }, - { - name: 'dns', - type: 'group', - fields: [ - { - name: 'type', - type: 'keyword', - }, - { - name: 'rrtype', - type: 'keyword', - }, - { - name: 'rrname', - type: 'keyword', - }, - { - name: 'rdata', - type: 'keyword', - }, - { - name: 'tx_id', - type: 'long', - }, - { - name: 'ttl', - type: 'long', - }, - { - name: 'rcode', - type: 'keyword', - }, - { - name: 'id', - type: 'long', - }, - ], - }, - { - name: 'flow_id', - type: 'keyword', - }, - { - name: 'email', - type: 'group', - fields: [ - { - name: 'status', - type: 'keyword', - }, - ], - }, - { - name: 'dest_ip', - type: 'alias', - path: 'destination.ip', - }, - { - name: 'icmp_code', - type: 'long', - }, - { - name: 'http', - type: 'group', - fields: [ - { - name: 'status', - type: 'alias', - path: 'http.response.status_code', - }, - { - name: 'redirect', - type: 'keyword', - }, - { - name: 'http_user_agent', - type: 'alias', - path: 'user_agent.original', - }, - { - name: 'protocol', - type: 'keyword', - }, - { - name: 'http_refer', - type: 'alias', - path: 'http.request.referrer', - }, - { - name: 'url', - type: 'alias', - path: 'url.original', - }, - { - name: 'hostname', - type: 'alias', - path: 'url.domain', - }, - { - name: 'length', - type: 'alias', - path: 'http.response.body.bytes', - }, - { - name: 'http_method', - type: 'alias', - path: 'http.request.method', - }, - { - name: 'http_content_type', - type: 'keyword', - }, - ], - }, - { - name: 'timestamp', - type: 'alias', - path: '@timestamp', - }, - { - name: 'in_iface', - type: 'keyword', - }, - { - name: 'alert', - type: 'group', - fields: [ - { - name: 'category', - type: 'keyword', - }, - { - name: 'severity', - type: 'alias', - path: 'event.severity', - }, - { - name: 'rev', - type: 'long', - }, - { - name: 'gid', - type: 'long', - }, - { - name: 'signature', - type: 'keyword', - }, - { - name: 'action', - type: 'alias', - path: 'event.outcome', - }, - { - name: 'signature_id', - type: 'long', - }, - ], - }, - { - name: 'ssh', - type: 'group', - fields: [ - { - name: 'client', - type: 'group', - fields: [ - { - name: 'proto_version', - type: 'keyword', - }, - { - name: 'software_version', - type: 'keyword', - }, - ], - }, - { - name: 'server', - type: 'group', - fields: [ - { - name: 'proto_version', - type: 'keyword', - }, - { - name: 'software_version', - type: 'keyword', - }, - ], - }, - ], - }, - { - name: 'stats', - type: 'group', - fields: [ - { - name: 'capture', - type: 'group', - fields: [ - { - name: 'kernel_packets', - type: 'long', - }, - { - name: 'kernel_drops', - type: 'long', - }, - { - name: 'kernel_ifdrops', - type: 'long', - }, - ], - }, - { - name: 'uptime', - type: 'long', - }, - { - name: 'detect', - type: 'group', - fields: [ - { - name: 'alert', - type: 'long', - }, - ], - }, - { - name: 'http', - type: 'group', - fields: [ - { - name: 'memcap', - type: 'long', - }, - { - name: 'memuse', - type: 'long', - }, - ], - }, - { - name: 'file_store', - type: 'group', - fields: [ - { - name: 'open_files', - type: 'long', - }, - ], - }, - { - name: 'defrag', - type: 'group', - fields: [ - { - name: 'max_frag_hits', - type: 'long', - }, - { - name: 'ipv4', - type: 'group', - fields: [ - { - name: 'timeouts', - type: 'long', - }, - { - name: 'fragments', - type: 'long', - }, - { - name: 'reassembled', - type: 'long', - }, - ], - }, - { - name: 'ipv6', - type: 'group', - fields: [ - { - name: 'timeouts', - type: 'long', - }, - { - name: 'fragments', - type: 'long', - }, - { - name: 'reassembled', - type: 'long', - }, - ], - }, - ], - }, - { - name: 'flow', - type: 'group', - fields: [ - { - name: 'tcp_reuse', - type: 'long', - }, - { - name: 'udp', - type: 'long', - }, - { - name: 'memcap', - type: 'long', - }, - { - name: 'emerg_mode_entered', - type: 'long', - }, - { - name: 'emerg_mode_over', - type: 'long', - }, - { - name: 'tcp', - type: 'long', - }, - { - name: 'icmpv6', - type: 'long', - }, - { - name: 'icmpv4', - type: 'long', - }, - { - name: 'spare', - type: 'long', - }, - { - name: 'memuse', - type: 'long', - }, - ], - }, - { - name: 'tcp', - type: 'group', - fields: [ - { - name: 'pseudo_failed', - type: 'long', - }, - { - name: 'ssn_memcap_drop', - type: 'long', - }, - { - name: 'insert_data_overlap_fail', - type: 'long', - }, - { - name: 'sessions', - type: 'long', - }, - { - name: 'pseudo', - type: 'long', - }, - { - name: 'synack', - type: 'long', - }, - { - name: 'insert_data_normal_fail', - type: 'long', - }, - { - name: 'syn', - type: 'long', - }, - { - name: 'memuse', - type: 'long', - }, - { - name: 'invalid_checksum', - type: 'long', - }, - { - name: 'segment_memcap_drop', - type: 'long', - }, - { - name: 'overlap', - type: 'long', - }, - { - name: 'insert_list_fail', - type: 'long', - }, - { - name: 'rst', - type: 'long', - }, - { - name: 'stream_depth_reached', - type: 'long', - }, - { - name: 'reassembly_memuse', - type: 'long', - }, - { - name: 'reassembly_gap', - type: 'long', - }, - { - name: 'overlap_diff_data', - type: 'long', - }, - { - name: 'no_flow', - type: 'long', - }, - ], - }, - { - name: 'decoder', - type: 'group', - fields: [ - { - name: 'avg_pkt_size', - type: 'long', - }, - { - name: 'bytes', - type: 'long', - }, - { - name: 'tcp', - type: 'long', - }, - { - name: 'raw', - type: 'long', - }, - { - name: 'ppp', - type: 'long', - }, - { - name: 'vlan_qinq', - type: 'long', - }, - { - name: 'null', - type: 'long', - }, - { - name: 'ltnull', - type: 'group', - fields: [ - { - name: 'unsupported_type', - type: 'long', - }, - { - name: 'pkt_too_small', - type: 'long', - }, - ], - }, - { - name: 'invalid', - type: 'long', - }, - { - name: 'gre', - type: 'long', - }, - { - name: 'ipv4', - type: 'long', - }, - { - name: 'ipv6', - type: 'long', - }, - { - name: 'pkts', - type: 'long', - }, - { - name: 'ipv6_in_ipv6', - type: 'long', - }, - { - name: 'ipraw', - type: 'group', - fields: [ - { - name: 'invalid_ip_version', - type: 'long', - }, - ], - }, - { - name: 'pppoe', - type: 'long', - }, - { - name: 'udp', - type: 'long', - }, - { - name: 'dce', - type: 'group', - fields: [ - { - name: 'pkt_too_small', - type: 'long', - }, - ], - }, - { - name: 'vlan', - type: 'long', - }, - { - name: 'sctp', - type: 'long', - }, - { - name: 'max_pkt_size', - type: 'long', - }, - { - name: 'teredo', - type: 'long', - }, - { - name: 'mpls', - type: 'long', - }, - { - name: 'sll', - type: 'long', - }, - { - name: 'icmpv6', - type: 'long', - }, - { - name: 'icmpv4', - type: 'long', - }, - { - name: 'erspan', - type: 'long', - }, - { - name: 'ethernet', - type: 'long', - }, - { - name: 'ipv4_in_ipv6', - type: 'long', - }, - { - name: 'ieee8021ah', - type: 'long', - }, - ], - }, - { - name: 'dns', - type: 'group', - fields: [ - { - name: 'memcap_global', - type: 'long', - }, - { - name: 'memcap_state', - type: 'long', - }, - { - name: 'memuse', - type: 'long', - }, - ], - }, - { - name: 'flow_mgr', - type: 'group', - fields: [ - { - name: 'rows_busy', - type: 'long', - }, - { - name: 'flows_timeout', - type: 'long', - }, - { - name: 'flows_notimeout', - type: 'long', - }, - { - name: 'rows_skipped', - type: 'long', - }, - { - name: 'closed_pruned', - type: 'long', - }, - { - name: 'new_pruned', - type: 'long', - }, - { - name: 'flows_removed', - type: 'long', - }, - { - name: 'bypassed_pruned', - type: 'long', - }, - { - name: 'est_pruned', - type: 'long', - }, - { - name: 'flows_timeout_inuse', - type: 'long', - }, - { - name: 'flows_checked', - type: 'long', - }, - { - name: 'rows_maxlen', - type: 'long', - }, - { - name: 'rows_checked', - type: 'long', - }, - { - name: 'rows_empty', - type: 'long', - }, - ], - }, - { - name: 'app_layer', - type: 'group', - fields: [ - { - name: 'flow', - type: 'group', - fields: [ - { - name: 'tls', - type: 'long', - }, - { - name: 'ftp', - type: 'long', - }, - { - name: 'http', - type: 'long', - }, - { - name: 'failed_udp', - type: 'long', - }, - { - name: 'dns_udp', - type: 'long', - }, - { - name: 'dns_tcp', - type: 'long', - }, - { - name: 'smtp', - type: 'long', - }, - { - name: 'failed_tcp', - type: 'long', - }, - { - name: 'msn', - type: 'long', - }, - { - name: 'ssh', - type: 'long', - }, - { - name: 'imap', - type: 'long', - }, - { - name: 'dcerpc_udp', - type: 'long', - }, - { - name: 'dcerpc_tcp', - type: 'long', - }, - { - name: 'smb', - type: 'long', - }, - ], - }, - { - name: 'tx', - type: 'group', - fields: [ - { - name: 'tls', - type: 'long', - }, - { - name: 'ftp', - type: 'long', - }, - { - name: 'http', - type: 'long', - }, - { - name: 'dns_udp', - type: 'long', - }, - { - name: 'dns_tcp', - type: 'long', - }, - { - name: 'smtp', - type: 'long', - }, - { - name: 'ssh', - type: 'long', - }, - { - name: 'dcerpc_udp', - type: 'long', - }, - { - name: 'dcerpc_tcp', - type: 'long', - }, - { - name: 'smb', - type: 'long', - }, - ], - }, - ], - }, - ], - }, - { - name: 'tls', - type: 'group', - fields: [ - { - name: 'notbefore', - type: 'date', - }, - { - name: 'issuerdn', - type: 'keyword', - }, - { - name: 'sni', - type: 'keyword', - }, - { - name: 'version', - type: 'keyword', - }, - { - name: 'session_resumed', - type: 'boolean', - }, - { - name: 'fingerprint', - type: 'keyword', - }, - { - name: 'serial', - type: 'keyword', - }, - { - name: 'notafter', - type: 'date', - }, - { - name: 'subject', - type: 'keyword', - }, - ], - }, - { - name: 'app_proto_ts', - type: 'keyword', - }, - { - name: 'flow', - type: 'group', - fields: [ - { - name: 'bytes_toclient', - type: 'alias', - path: 'destination.bytes', - }, - { - name: 'start', - type: 'alias', - path: 'event.start', - }, - { - name: 'pkts_toclient', - type: 'alias', - path: 'destination.packets', - }, - { - name: 'age', - type: 'long', - }, - { - name: 'state', - type: 'keyword', - }, - { - name: 'bytes_toserver', - type: 'alias', - path: 'source.bytes', - }, - { - name: 'reason', - type: 'keyword', - }, - { - name: 'pkts_toserver', - type: 'alias', - path: 'source.packets', - }, - { - name: 'end', - type: 'date', - }, - { - name: 'alerted', - type: 'boolean', - }, - ], - }, - { - name: 'app_proto', - type: 'alias', - path: 'network.protocol', - }, - { - name: 'tx_id', - type: 'long', - }, - { - name: 'app_proto_tc', - type: 'keyword', - }, - { - name: 'smtp', - type: 'group', - fields: [ - { - name: 'rcpt_to', - type: 'keyword', - }, - { - name: 'mail_from', - type: 'keyword', - }, - { - name: 'helo', - type: 'keyword', - }, - ], - }, - { - name: 'app_proto_expected', - type: 'keyword', - }, - { - name: 'flags', - type: 'group', - fields: [], - }, - ], - }, - ], - }, - ], - }, - { - key: 'zeek', - title: 'Zeek', - description: 'Module for handling logs produced by Zeek/Bro\n', - fields: [ - { - name: 'zeek', - type: 'group', - description: 'Fields from Zeek/Bro logs after normalization\n', - fields: [ - { - name: 'session_id', - type: 'keyword', - description: 'A unique identifier of the session\n', - }, - { - name: 'capture_loss', - type: 'group', - description: 'Fields exported by the Zeek capture_loss log\n', - fields: [ - { - name: 'ts_delta', - type: 'integer', - description: 'The time delay between this measurement and the last.\n', - }, - { - name: 'peer', - type: 'keyword', - description: - 'In the event that there are multiple Bro instances logging to the same host, this distinguishes each peer with its individual name.\n', - }, - { - name: 'gaps', - type: 'integer', - description: 'Number of missed ACKs from the previous measurement interval.\n', - }, - { - name: 'acks', - type: 'integer', - description: 'Total number of ACKs seen in the previous measurement interval.\n', - }, - { - name: 'percent_lost', - type: 'double', - description: "Percentage of ACKs seen where the data being ACKed wasn't seen.\n", - }, - ], - }, - { - name: 'connection', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek Connection log\n', - fields: [ - { - name: 'local_orig', - type: 'boolean', - description: 'Indicates whether the session is originated locally.\n', - }, - { - name: 'local_resp', - type: 'boolean', - description: 'Indicates whether the session is responded locally.\n', - }, - { - name: 'missed_bytes', - type: 'long', - description: 'Missed bytes for the session.\n', - }, - { - name: 'state', - type: 'keyword', - description: 'Code indicating the state of the session.\n', - }, - { - name: 'state_message', - type: 'keyword', - description: 'The state of the session.\n', - }, - { - name: 'icmp', - type: 'group', - fields: [ - { - name: 'type', - type: 'integer', - description: 'ICMP message type.\n', - }, - { - name: 'code', - type: 'integer', - description: 'ICMP message code.\n', - }, - ], - }, - { - name: 'history', - type: 'keyword', - description: 'Flags indicating the history of the session.\n', - }, - { - name: 'vlan', - type: 'integer', - description: 'VLAN identifier.\n', - }, - { - name: 'inner_vlan', - type: 'integer', - description: 'VLAN identifier.\n', - }, - ], - }, - { - name: 'dce_rpc', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek DCE_RPC log\n', - fields: [ - { - name: 'rtt', - type: 'integer', - description: - "Round trip time from the request to the response. If either the request or response wasn't seen, this will be null.\n", - }, - { - name: 'named_pipe', - type: 'keyword', - description: 'Remote pipe name.\n', - }, - { - name: 'endpoint', - type: 'keyword', - description: 'Endpoint name looked up from the uuid.\n', - }, - { - name: 'operation', - type: 'keyword', - description: 'Operation seen in the call.\n', - }, - ], - }, - { - name: 'dhcp', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek DHCP log\n', - fields: [ - { - name: 'domain', - type: 'keyword', - description: 'Domain given by the server in option 15.\n', - }, - { - name: 'duration', - type: 'double', - description: - 'Duration of the DHCP session representing the time from the first\nmessage to the last, in seconds.\n', - }, - { - name: 'hostname', - type: 'keyword', - description: 'Name given by client in Hostname option 12.\n', - }, - { - name: 'client_fqdn', - type: 'keyword', - description: 'FQDN given by client in Client FQDN option 81.\n', - }, - { - name: 'lease_time', - type: 'integer', - description: 'IP address lease interval in seconds.\n', - }, - { - name: 'address', - type: 'group', - description: 'Addresses seen in this DHCP exchange.\n', - fields: [ - { - name: 'assigned', - type: 'ip', - description: 'IP address assigned by the server.\n', - }, - { - name: 'client', - type: 'ip', - description: - 'IP address of the client. If a transaction is only a client sending\nINFORM messages then there is no lease information exchanged so this\nis helpful to know who sent the messages. Getting an address in this\nfield does require that the client sources at least one DHCP message\nusing a non-broadcast address.\n', - }, - { - name: 'mac', - type: 'keyword', - description: "Client's hardware address.\n", - }, - { - name: 'requested', - type: 'ip', - description: 'IP address requested by the client.\n', - }, - { - name: 'server', - type: 'ip', - description: 'IP address of the DHCP server.\n', - }, - ], - }, - { - name: 'msg', - type: 'group', - fields: [ - { - name: 'types', - type: 'keyword', - description: 'List of DHCP message types seen in this exchange.\n', - }, - { - name: 'origin', - type: 'ip', - description: - '(present if policy/protocols/dhcp/msg-orig.bro is loaded)\nThe address that originated each message from the msg.types field.\n', - }, - { - name: 'client', - type: 'keyword', - description: - 'Message typically accompanied with a DHCP_DECLINE so the client can\ntell the server why it rejected an address.\n', - }, - { - name: 'server', - type: 'keyword', - description: - 'Message typically accompanied with a DHCP_NAK to let the client know\nwhy it rejected the request.\n', - }, - ], - }, - { - name: 'software', - type: 'group', - fields: [ - { - name: 'client', - type: 'keyword', - description: - '(present if policy/protocols/dhcp/software.bro is loaded)\nSoftware reported by the client in the vendor_class option.\n', - }, - { - name: 'server', - type: 'keyword', - description: - '(present if policy/protocols/dhcp/software.bro is loaded)\nSoftware reported by the client in the vendor_class option.\n', - }, - ], - }, - { - name: 'id', - type: 'group', - fields: [ - { - name: 'circuit', - type: 'keyword', - description: - '(present if policy/protocols/dhcp/sub-opts.bro is loaded)\nAdded by DHCP relay agents which terminate switched or permanent\ncircuits. It encodes an agent-local identifier of the circuit from\nwhich a DHCP client-to-server packet was received. Typically it\nshould represent a router or switch interface number.\n', - }, - { - name: 'remote_agent', - type: 'keyword', - description: - '(present if policy/protocols/dhcp/sub-opts.bro is loaded)\nA globally unique identifier added by relay agents to identify the\nremote host end of the circuit.\n', - }, - { - name: 'subscriber', - type: 'keyword', - description: - "(present if policy/protocols/dhcp/sub-opts.bro is loaded)\nThe subscriber ID is a value independent of the physical network\nconfiguration so that a customer's DHCP configuration can be given\nto them correctly no matter where they are physically connected.\n", - }, - ], - }, - ], - }, - { - name: 'dnp3', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek SSH log\n', - fields: [ - { - name: 'function', - type: 'group', - fields: [ - { - name: 'request', - type: 'keyword', - description: 'The name of the function message in the request.\n', - }, - { - name: 'reply', - type: 'keyword', - description: 'The name of the function message in the reply.\n', - }, - ], - }, - { - name: 'id', - type: 'integer', - description: "The response's internal indication number.\n", - }, - ], - }, - { - name: 'dns', - type: 'group', - description: 'Fields exported by the Zeek DNS log\n', - fields: [ - { - name: 'trans_id', - type: 'keyword', - description: 'DNS transaction identifier.\n', - }, - { - name: 'rtt', - type: 'double', - description: 'Round trip time for the query and response.\n', - }, - { - name: 'query', - type: 'keyword', - description: 'The domain name that is the subject of the DNS query.\n', - }, - { - name: 'qclass', - type: 'long', - description: 'The QCLASS value specifying the class of the query.\n', - }, - { - name: 'qclass_name', - type: 'keyword', - description: 'A descriptive name for the class of the query.\n', - }, - { - name: 'qtype', - type: 'long', - description: 'A QTYPE value specifying the type of the query.\n', - }, - { - name: 'qtype_name', - type: 'keyword', - description: 'A descriptive name for the type of the query.\n', - }, - { - name: 'rcode', - type: 'long', - description: 'The response code value in DNS response messages.\n', - }, - { - name: 'rcode_name', - type: 'keyword', - description: 'A descriptive name for the response code value.\n', - }, - { - name: 'AA', - type: 'boolean', - description: - 'The Authoritative Answer bit for response messages specifies that the responding\nname server is an authority for the domain name in the question section.\n', - }, - { - name: 'TC', - type: 'boolean', - description: 'The Truncation bit specifies that the message was truncated.\n', - }, - { - name: 'RD', - type: 'boolean', - description: - 'The Recursion Desired bit in a request message indicates that the client\nwants recursive service for this query.\n', - }, - { - name: 'RA', - type: 'boolean', - description: - 'The Recursion Available bit in a response message indicates that the name\nserver supports recursive queries.\n', - }, - { - name: 'answers', - type: 'keyword', - description: 'The set of resource descriptions in the query answer.\n', - }, - { - name: 'TTLs', - type: 'double', - description: - 'The caching intervals of the associated RRs described by the answers field.\n', - }, - { - name: 'rejected', - type: 'boolean', - description: 'Indicates whether the DNS query was rejected by the server.\n', - }, - { - name: 'total_answers', - type: 'integer', - description: 'The total number of resource records in the reply.\n', - }, - { - name: 'total_replies', - type: 'integer', - description: 'The total number of resource records in the reply message.\n', - }, - { - name: 'saw_query', - type: 'boolean', - description: 'Whether the full DNS query has been seen.\n', - }, - { - name: 'saw_reply', - type: 'boolean', - description: 'Whether the full DNS reply has been seen.\n', - }, - ], - }, - { - name: 'dpd', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek DPD log\n', - fields: [ - { - name: 'analyzer', - type: 'keyword', - description: 'The analyzer that generated the violation.\n', - }, - { - name: 'failure_reason', - type: 'keyword', - description: 'The textual reason for the analysis failure.\n', - }, - { - name: 'packet_segment', - type: 'keyword', - description: - '(present if policy/frameworks/dpd/packet-segment-logging.bro is loaded)\nA chunk of the payload that most likely resulted in the protocol violation.\n', - }, - ], - }, - { - name: 'files', - type: 'group', - description: 'Fields exported by the Zeek Files log.\n', - fields: [ - { - name: 'fuid', - type: 'keyword', - description: 'A file unique identifier.\n', - }, - { - name: 'tx_host', - type: 'ip', - description: 'The host that transferred the file.\n', - }, - { - name: 'rx_host', - type: 'ip', - description: 'The host that received the file.\n', - }, - { - name: 'session_ids', - type: 'keyword', - description: 'The sessions that have this file.\n', - }, - { - name: 'source', - type: 'keyword', - description: - 'An identification of the source of the file data. E.g. it may be a network protocol\nover which it was transferred, or a local file path which was read, or some other\ninput source.\n', - }, - { - name: 'depth', - type: 'long', - description: - 'A value to represent the depth of this file in relation to its source. In SMTP, it\nis the depth of the MIME attachment on the message. In HTTP, it is the depth of the\nrequest within the TCP connection.\n', - }, - { - name: 'analyzers', - type: 'keyword', - description: 'A set of analysis types done during the file analysis.\n', - }, - { - name: 'mime_type', - type: 'keyword', - description: 'Mime type of the file.\n', - }, - { - name: 'filename', - type: 'keyword', - description: 'Name of the file if available.\n', - }, - { - name: 'local_orig', - type: 'boolean', - description: - 'If the source of this file is a network connection, this field indicates if the data\noriginated from the local network or not.\n', - }, - { - name: 'is_orig', - type: 'boolean', - description: - 'If the source of this file is a network connection, this field indicates if the file is\nbeing sent by the originator of the connection or the responder.\n', - }, - { - name: 'duration', - type: 'double', - description: - 'The duration the file was analyzed for. Not the duration of the session.\n', - }, - { - name: 'seen_bytes', - type: 'long', - description: 'Number of bytes provided to the file analysis engine for the file.\n', - }, - { - name: 'total_bytes', - type: 'long', - description: 'Total number of bytes that are supposed to comprise the full file.\n', - }, - { - name: 'missing_bytes', - type: 'long', - description: - 'The number of bytes in the file stream that were completely missed during the process\nof analysis.\n', - }, - { - name: 'overflow_bytes', - type: 'long', - description: - "The number of bytes in the file stream that were not delivered to stream file analyzers.\nThis could be overlapping bytes or bytes that couldn't be reassembled.\n", - }, - { - name: 'timedout', - type: 'boolean', - description: 'Whether the file analysis timed out at least once for the file.\n', - }, - { - name: 'parent_fuid', - type: 'keyword', - description: - 'Identifier associated with a container file from which this one was extracted as part of\nthe file analysis.\n', - }, - { - name: 'md5', - type: 'keyword', - description: 'An MD5 digest of the file contents.\n', - }, - { - name: 'sha1', - type: 'keyword', - description: 'A SHA1 digest of the file contents.\n', - }, - { - name: 'sha256', - type: 'keyword', - description: 'A SHA256 digest of the file contents.\n', - }, - { - name: 'extracted', - type: 'keyword', - description: 'Local filename of extracted file.\n', - }, - { - name: 'extracted_cutoff', - type: 'boolean', - description: - 'Indicate whether the file being extracted was cut off hence not extracted completely.\n', - }, - { - name: 'extracted_size', - type: 'long', - description: 'The number of bytes extracted to disk.\n', - }, - { - name: 'entropy', - type: 'double', - description: 'The information density of the contents of the file.\n', - }, - ], - }, - { - name: 'ftp', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek FTP log\n', - fields: [ - { - name: 'user', - type: 'keyword', - description: 'User name for the current FTP session.\n', - }, - { - name: 'password', - type: 'keyword', - description: 'Password for the current FTP session if captured.\n', - }, - { - name: 'command', - type: 'keyword', - description: 'Command given by the client.\n', - }, - { - name: 'arg', - type: 'keyword', - description: 'Argument for the command if one is given.\n', - }, - { - name: 'file', - type: 'group', - fields: [ - { - name: 'size', - type: 'long', - description: 'Size of the file if the command indicates a file transfer.\n', - }, - { - name: 'mime_type', - type: 'keyword', - description: 'Sniffed mime type of file.\n', - }, - { - name: 'fuid', - type: 'keyword', - description: - '(present if base/protocols/ftp/files.bro is loaded)\nFile unique ID.\n', - }, - ], - }, - { - name: 'reply', - type: 'group', - fields: [ - { - name: 'code', - type: 'integer', - description: 'Reply code from the server in response to the command.\n', - }, - { - name: 'msg', - type: 'keyword', - description: 'Reply message from the server in response to the command.\n', - }, - ], - }, - { - name: 'data_channel', - type: 'group', - description: 'Expected FTP data channel.\n', - fields: [ - { - name: 'passive', - type: 'boolean', - description: 'Whether PASV mode is toggled for control channel.\n', - }, - { - name: 'originating_host', - type: 'ip', - description: 'The host that will be initiating the data connection.\n', - }, - { - name: 'response_host', - type: 'ip', - description: 'The host that will be accepting the data connection.\n', - }, - { - name: 'response_port', - type: 'integer', - description: - 'The port at which the acceptor is listening for the data connection.\n', - }, - ], - }, - { - name: 'cwd', - type: 'keyword', - description: - "Current working directory that this session is in. By making the default value '.', we can indicate that unless something more concrete is discovered that the existing but unknown directory is ok to use.\n", - }, - { - name: 'cmdarg', - type: 'group', - description: 'Command that is currently waiting for a response.\n', - fields: [ - { - name: 'cmd', - type: 'keyword', - description: 'Command.\n', - }, - { - name: 'arg', - type: 'keyword', - description: 'Argument for the command if one was given.\n', - }, - { - name: 'seq', - type: 'integer', - description: 'Counter to track how many commands have been executed.\n', - }, - ], - }, - { - name: 'pending_commands', - type: 'integer', - description: - 'Queue for commands that have been sent but not yet responded to are tracked here.\n', - }, - { - name: 'passive', - type: 'boolean', - description: 'Indicates if the session is in active or passive mode.\n', - }, - { - name: 'capture_password', - type: 'boolean', - description: 'Determines if the password will be captured for this request.\n', - }, - { - name: 'last_auth_requested', - type: 'keyword', - description: - 'present if base/protocols/ftp/gridftp.bro is loaded.\nLast authentication/security mechanism that was used.\n', - }, - ], - }, - { - name: 'http', - type: 'group', - description: 'Fields exported by the Zeek HTTP log\n', - fields: [ - { - name: 'trans_depth', - type: 'integer', - description: - 'Represents the pipelined depth into the connection of this request/response transaction.\n', - }, - { - name: 'status_msg', - type: 'keyword', - description: 'Status message returned by the server.\n', - }, - { - name: 'info_code', - type: 'integer', - description: 'Last seen 1xx informational reply code returned by the server.\n', - }, - { - name: 'info_msg', - type: 'keyword', - description: 'Last seen 1xx informational reply message returned by the server.\n', - }, - { - name: 'tags', - type: 'keyword', - description: - 'A set of indicators of various attributes discovered and related to a particular\nrequest/response pair.\n', - }, - { - name: 'password', - type: 'keyword', - description: 'Password if basic-auth is performed for the request.\n', - }, - { - name: 'captured_password', - type: 'boolean', - description: 'Determines if the password will be captured for this request.\n', - }, - { - name: 'proxied', - type: 'keyword', - description: - 'All of the headers that may indicate if the HTTP request was proxied.\n', - }, - { - name: 'range_request', - type: 'boolean', - description: - 'Indicates if this request can assume 206 partial content in response.\n', - }, - { - name: 'client_header_names', - type: 'keyword', - description: - 'The vector of HTTP header names sent by the client. No header values\nare included here, just the header names.\n', - }, - { - name: 'server_header_names', - type: 'keyword', - description: - 'The vector of HTTP header names sent by the server. No header values\nare included here, just the header names.\n', - }, - { - name: 'orig_fuids', - type: 'keyword', - description: 'An ordered vector of file unique IDs from the originator.\n', - }, - { - name: 'orig_mime_types', - type: 'keyword', - description: 'An ordered vector of mime types from the originator.\n', - }, - { - name: 'orig_filenames', - type: 'keyword', - description: 'An ordered vector of filenames from the originator.\n', - }, - { - name: 'resp_fuids', - type: 'keyword', - description: 'An ordered vector of file unique IDs from the responder.\n', - }, - { - name: 'resp_mime_types', - type: 'keyword', - description: 'An ordered vector of mime types from the responder.\n', - }, - { - name: 'resp_filenames', - type: 'keyword', - description: 'An ordered vector of filenames from the responder.\n', - }, - { - name: 'orig_mime_depth', - type: 'integer', - description: 'Current number of MIME entities in the HTTP request message body.\n', - }, - { - name: 'resp_mime_depth', - type: 'integer', - description: 'Current number of MIME entities in the HTTP response message body.\n', - }, - ], - }, - { - name: 'intel', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek Intel log.\n', - fields: [ - { - name: 'seen', - type: 'group', - fields: [ - { - name: 'indicator', - type: 'keyword', - description: 'The intelligence indicator.\n', - }, - { - name: 'indicator_type', - type: 'keyword', - description: 'The type of data the indicator represents.\n', - }, - { - name: 'host', - type: 'keyword', - description: - 'If the indicator type was Intel::ADDR, then this field will be present.\n', - }, - { - name: 'conn', - type: 'keyword', - description: - 'If the data was discovered within a connection, the connection record should go here to give context to the data.\n', - }, - { - name: 'where', - type: 'keyword', - description: 'Where the data was discovered.\n', - }, - { - name: 'node', - type: 'keyword', - description: 'The name of the node where the match was discovered.\n', - }, - { - name: 'uid', - type: 'keyword', - description: - 'If the data was discovered within a connection, the connection uid should go here to give context to the data. If the conn field is provided, this will be automatically filled out.\n', - }, - { - name: 'f', - type: 'object', - description: - 'If the data was discovered within a file, the file record should go here to provide context to the data.\n', - }, - { - name: 'fuid', - type: 'keyword', - description: - 'If the data was discovered within a file, the file uid should go here to provide context to the data. If the file record f is provided, this will be automatically filled out.\n', - }, - ], - }, - { - name: 'matched', - type: 'keyword', - description: - 'Event to represent a match in the intelligence data from data that was seen.\n', - }, - { - name: 'sources', - type: 'keyword', - description: 'Sources which supplied data for this match.\n', - }, - { - name: 'fuid', - type: 'keyword', - description: - 'If a file was associated with this intelligence hit, this is the uid for the file.\n', - }, - { - name: 'file_mime_type', - type: 'keyword', - description: - 'A mime type if the intelligence hit is related to a file. If the $f field is provided this will be automatically filled out.\n', - }, - { - name: 'file_desc', - type: 'keyword', - description: - 'Frequently files can be described to give a bit more context. If the $f field is provided this field will be automatically filled out.\n', - }, - ], - }, - { - name: 'irc', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek IRC log\n', - fields: [ - { - name: 'nick', - type: 'keyword', - description: 'Nickname given for the connection.\n', - }, - { - name: 'user', - type: 'keyword', - description: 'Username given for the connection.\n', - }, - { - name: 'command', - type: 'keyword', - description: 'Command given by the client.\n', - }, - { - name: 'value', - type: 'keyword', - description: 'Value for the command given by the client.\n', - }, - { - name: 'addl', - type: 'keyword', - description: 'Any additional data for the command.\n', - }, - { - name: 'dcc', - type: 'group', - fields: [ - { - name: 'file', - type: 'group', - fields: [ - { - name: 'name', - type: 'keyword', - description: - 'Present if base/protocols/irc/dcc-send.bro is loaded.\nDCC filename requested.\n', - }, - { - name: 'size', - type: 'long', - description: - 'Present if base/protocols/irc/dcc-send.bro is loaded.\nSize of the DCC transfer as indicated by the sender.\n', - }, - ], - }, - { - name: 'mime_type', - type: 'keyword', - description: - 'present if base/protocols/irc/dcc-send.bro is loaded.\nSniffed mime type of the file.\n', - }, - ], - }, - { - name: 'fuid', - type: 'keyword', - description: - 'present if base/protocols/irc/files.bro is loaded.\nFile unique ID.\n', - }, - ], - }, - { - name: 'kerberos', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek Kerberos log\n', - fields: [ - { - name: 'request_type', - type: 'keyword', - description: - 'Request type - Authentication Service (AS) or Ticket Granting Service (TGS).\n', - }, - { - name: 'client', - type: 'keyword', - description: 'Client name.\n', - }, - { - name: 'service', - type: 'keyword', - description: 'Service name.\n', - }, - { - name: 'success', - type: 'boolean', - description: 'Request result.\n', - }, - { - name: 'error', - type: 'group', - fields: [ - { - name: 'code', - type: 'integer', - description: 'Error code.\n', - }, - { - name: 'msg', - type: 'keyword', - description: 'Error message.\n', - }, - ], - }, - { - name: 'valid', - type: 'group', - fields: [ - { - name: 'from', - type: 'date', - description: 'Ticket valid from.\n', - }, - { - name: 'until', - type: 'date', - description: 'Ticket valid until.\n', - }, - { - name: 'days', - type: 'integer', - description: 'Number of days the ticket is valid for.\n', - }, - ], - }, - { - name: 'cipher', - type: 'keyword', - description: 'Ticket encryption type.\n', - }, - { - name: 'forwardable', - type: 'boolean', - description: 'Forwardable ticket requested.\n', - }, - { - name: 'renewable', - type: 'boolean', - description: 'Renewable ticket requested.\n', - }, - { - name: 'ticket', - type: 'group', - fields: [ - { - name: 'auth', - type: 'keyword', - description: 'Hash of ticket used to authorize request/transaction.\n', - }, - { - name: 'new', - type: 'keyword', - description: 'Hash of ticket returned by the KDC.\n', - }, - ], - }, - { - name: 'cert', - type: 'group', - fields: [ - { - name: 'client', - type: 'group', - fields: [ - { - name: 'value', - type: 'keyword', - description: 'Client certificate.\n', - }, - { - name: 'fuid', - type: 'keyword', - description: 'File unique ID of client cert.\n', - }, - { - name: 'subject', - type: 'keyword', - description: 'Subject of client certificate.\n', - }, - ], - }, - { - name: 'server', - type: 'group', - fields: [ - { - name: 'value', - type: 'keyword', - description: 'Server certificate.\n', - }, - { - name: 'fuid', - type: 'keyword', - description: 'File unique ID of server certificate.\n', - }, - { - name: 'subject', - type: 'keyword', - description: 'Subject of server certificate.\n', - }, - ], - }, - ], - }, - ], - }, - { - name: 'modbus', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek modbus log.\n', - fields: [ - { - name: 'function', - type: 'keyword', - description: 'The name of the function message that was sent.\n', - }, - { - name: 'exception', - type: 'keyword', - description: 'The exception if the response was a failure.\n', - }, - { - name: 'track_address', - type: 'integer', - description: - 'Present if policy/protocols/modbus/track-memmap.bro is loaded.\nModbus track address.\n', - }, - ], - }, - { - name: 'mysql', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek MySQL log.\n', - fields: [ - { - name: 'cmd', - type: 'keyword', - description: 'The command that was issued.\n', - }, - { - name: 'arg', - type: 'keyword', - description: 'The argument issued to the command.\n', - }, - { - name: 'success', - type: 'boolean', - description: 'Whether the command succeeded.\n', - }, - { - name: 'rows', - type: 'integer', - description: 'The number of affected rows, if any.\n', - }, - { - name: 'response', - type: 'keyword', - description: 'Server message, if any.\n', - }, - ], - }, - { - name: 'notice', - type: 'group', - description: 'Fields exported by the Zeek Notice log.\n', - fields: [ - { - name: 'connection_id', - type: 'keyword', - description: 'Identifier of the related connection session.\n', - }, - { - name: 'icmp_id', - type: 'keyword', - description: 'Identifier of the related ICMP session.\n', - }, - { - name: 'file.id', - type: 'keyword', - description: - 'An identifier associated with a single file that is related to this notice.\n', - }, - { - name: 'file.parent_id', - type: 'keyword', - description: - 'Identifier associated with a container file from which this one was extracted.\n', - }, - { - name: 'file.source', - type: 'keyword', - description: - 'An identification of the source of the file data. E.g. it may be a network protocol\nover which it was transferred, or a local file path which was read, or some other\ninput source.\n', - }, - { - name: 'file.mime_type', - type: 'keyword', - description: 'A mime type if the notice is related to a file.\n', - }, - { - name: 'file.is_orig', - type: 'boolean', - description: - 'If the source of this file is a network connection, this field indicates if the file is\nbeing sent by the originator of the connection or the responder.\n', - }, - { - name: 'file.seen_bytes', - type: 'long', - description: 'Number of bytes provided to the file analysis engine for the file.\n', - }, - { - name: 'ffile.total_bytes', - type: 'long', - description: 'Total number of bytes that are supposed to comprise the full file.\n', - }, - { - name: 'file.missing_bytes', - type: 'long', - description: - 'The number of bytes in the file stream that were completely missed during the process\nof analysis.\n', - }, - { - name: 'file.overflow_bytes', - type: 'long', - description: - "The number of bytes in the file stream that were not delivered to stream file analyzers.\nThis could be overlapping bytes or bytes that couldn't be reassembled.\n", - }, - { - name: 'fuid', - type: 'keyword', - description: 'A file unique ID if this notice is related to a file.\n', - }, - { - name: 'note', - type: 'keyword', - description: 'The type of the notice.\n', - }, - { - name: 'msg', - type: 'keyword', - description: 'The human readable message for the notice.\n', - }, - { - name: 'sub', - type: 'keyword', - description: 'The human readable sub-message.\n', - }, - { - name: 'n', - type: 'long', - description: 'Associated count, or a status code.\n', - }, - { - name: 'peer_name', - type: 'keyword', - description: 'Name of remote peer that raised this notice.\n', - }, - { - name: 'peer_descr', - type: 'text', - description: 'Textual description for the peer that raised this notice.\n', - }, - { - name: 'actions', - type: 'keyword', - description: 'The actions which have been applied to this notice.\n', - }, - { - name: 'email_body_sections', - type: 'text', - description: - 'By adding chunks of text into this element, other scripts can expand on notices\nthat are being emailed.\n', - }, - { - name: 'email_delay_tokens', - type: 'keyword', - description: - 'Adding a string token to this set will cause the built-in emailing functionality\nto delay sending the email either the token has been removed or the email\nhas been delayed for the specified time duration.\n', - }, - { - name: 'identifier', - type: 'keyword', - description: - 'This field is provided when a notice is generated for the purpose of deduplicating notices.\n', - }, - { - name: 'suppress_for', - type: 'double', - description: - 'This field indicates the length of time that this unique notice should be suppressed.\n', - }, - { - name: 'dropped', - type: 'boolean', - description: - 'Indicate if the source IP address was dropped and denied network access.\n', - }, - ], - }, - { - name: 'ntlm', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek NTLM log.\n', - fields: [ - { - name: 'domain', - type: 'keyword', - description: 'Domain name given by the client.\n', - }, - { - name: 'hostname', - type: 'keyword', - description: 'Hostname given by the client.\n', - }, - { - name: 'success', - type: 'boolean', - description: 'Indicate whether or not the authentication was successful.\n', - }, - { - name: 'username', - type: 'keyword', - description: 'Username given by the client.\n', - }, - { - name: 'server', - type: 'group', - fields: [ - { - name: 'name', - type: 'group', - fields: [ - { - name: 'dns', - type: 'keyword', - description: 'DNS name given by the server in a CHALLENGE.\n', - }, - { - name: 'netbios', - type: 'keyword', - description: 'NetBIOS name given by the server in a CHALLENGE.\n', - }, - { - name: 'tree', - type: 'keyword', - description: 'Tree name given by the server in a CHALLENGE.\n', - }, - ], - }, - ], - }, - ], - }, - { - name: 'ocsp', - type: 'group', - default_field: false, - description: - 'Fields exported by the Zeek OCSP log\nOnline Certificate Status Protocol (OCSP). Only created if policy script is loaded.\n', - fields: [ - { - name: 'file_id', - type: 'keyword', - description: 'File id of the OCSP reply.\n', - }, - { - name: 'hash', - type: 'group', - fields: [ - { - name: 'algorithm', - type: 'keyword', - description: - 'Hash algorithm used to generate issuerNameHash and issuerKeyHash.\n', - }, - { - name: 'issuer', - type: 'group', - fields: [ - { - name: 'name', - type: 'keyword', - description: "Hash of the issuer's distingueshed name.\n", - }, - { - name: 'key', - type: 'keyword', - description: "Hash of the issuer's public key.\n", - }, - ], - }, - ], - }, - { - name: 'serial_number', - type: 'keyword', - description: 'Serial number of the affected certificate.\n', - }, - { - name: 'status', - type: 'keyword', - description: 'Status of the affected certificate.\n', - }, - { - name: 'revoke', - type: 'group', - fields: [ - { - name: 'time', - type: 'date', - description: 'Time at which the certificate was revoked.\n', - }, - { - name: 'reason', - type: 'keyword', - description: 'Reason for which the certificate was revoked.\n', - }, - ], - }, - { - name: 'update', - type: 'group', - fields: [ - { - name: 'this', - type: 'date', - description: - 'The time at which the status being shows is known to have been correct.\n', - }, - { - name: 'next', - type: 'date', - description: - 'The latest time at which new information about the status of the certificate will be available.\n', - }, - ], - }, - ], - }, - { - name: 'pe', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek pe log.\n', - fields: [ - { - name: 'client', - type: 'keyword', - description: "The client's version string.\n", - }, - { - name: 'id', - type: 'keyword', - description: 'File id of this portable executable file.\n', - }, - { - name: 'machine', - type: 'keyword', - description: 'The target machine that the file was compiled for.\n', - }, - { - name: 'compile_time', - type: 'date', - description: 'The time that the file was created at.\n', - }, - { - name: 'os', - type: 'keyword', - description: 'The required operating system.\n', - }, - { - name: 'subsystem', - type: 'keyword', - description: 'The subsystem that is required to run this file.\n', - }, - { - name: 'is_exe', - type: 'boolean', - description: 'Is the file an executable, or just an object file?\n', - }, - { - name: 'is_64bit', - type: 'boolean', - description: 'Is the file a 64-bit executable?\n', - }, - { - name: 'uses_aslr', - type: 'boolean', - description: 'Does the file support Address Space Layout Randomization?\n', - }, - { - name: 'uses_dep', - type: 'boolean', - description: 'Does the file support Data Execution Prevention?\n', - }, - { - name: 'uses_code_integrity', - type: 'boolean', - description: 'Does the file enforce code integrity checks?\n', - }, - { - name: 'uses_seh', - type: 'boolean', - description: 'Does the file use structured exception handing?\n', - }, - { - name: 'has_import_table', - type: 'boolean', - description: 'Does the file have an import table?\n', - }, - { - name: 'has_export_table', - type: 'boolean', - description: 'Does the file have an export table?\n', - }, - { - name: 'has_cert_table', - type: 'boolean', - description: 'Does the file have an attribute certificate table?\n', - }, - { - name: 'has_debug_data', - type: 'boolean', - description: 'Does the file have a debug table?\n', - }, - { - name: 'section_names', - type: 'keyword', - description: 'The names of the sections, in order.\n', - }, - ], - }, - { - name: 'radius', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek Radius log.\n', - fields: [ - { - name: 'username', - type: 'keyword', - description: 'The username, if present.\n', - }, - { - name: 'mac', - type: 'keyword', - description: 'MAC address, if present.\n', - }, - { - name: 'framed_addr', - type: 'ip', - description: - 'The address given to the network access server, if present. This is only a hint from the RADIUS server and the network access server is not required to honor the address.\n', - }, - { - name: 'remote_ip', - type: 'ip', - description: - 'Remote IP address, if present. This is collected from the Tunnel-Client-Endpoint attribute.\n', - }, - { - name: 'connect_info', - type: 'keyword', - description: 'Connect info, if present.\n', - }, - { - name: 'reply_msg', - type: 'keyword', - description: - 'Reply message from the server challenge. This is frequently shown to the user authenticating.\n', - }, - { - name: 'result', - type: 'keyword', - description: 'Successful or failed authentication.\n', - }, - { - name: 'ttl', - type: 'integer', - description: - 'The duration between the first request and either the "Access-Accept" message or an error. If the field is empty, it means that either the request or response was not seen.\n', - }, - { - name: 'logged', - type: 'boolean', - description: 'Whether this has already been logged and can be ignored.\n', - }, - ], - }, - { - name: 'rdp', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek RDP log.\n', - fields: [ - { - name: 'cookie', - type: 'keyword', - description: - 'Cookie value used by the client machine. This is typically a username.\n', - }, - { - name: 'result', - type: 'keyword', - description: - "Status result for the connection. It's a mix between RDP negotation failure messages and GCC server create response messages.\n", - }, - { - name: 'security_protocol', - type: 'keyword', - description: 'Security protocol chosen by the server.\n', - }, - { - name: 'keyboard_layout', - type: 'keyword', - description: 'Keyboard layout (language) of the client machine.\n', - }, - { - name: 'client', - type: 'group', - fields: [ - { - name: 'build', - type: 'keyword', - description: 'RDP client version used by the client machine.\n', - }, - { - name: 'client_name', - type: 'keyword', - description: 'Name of the client machine.\n', - }, - { - name: 'product_id', - type: 'keyword', - description: 'Product ID of the client machine.\n', - }, - ], - }, - { - name: 'desktop', - type: 'group', - fields: [ - { - name: 'width', - type: 'integer', - description: 'Desktop width of the client machine.\n', - }, - { - name: 'height', - type: 'integer', - description: 'Desktop height of the client machine.\n', - }, - { - name: 'color_depth', - type: 'keyword', - description: - 'The color depth requested by the client in the high_color_depth field.\n', - }, - ], - }, - { - name: 'cert', - type: 'group', - fields: [ - { - name: 'type', - type: 'keyword', - description: - 'If the connection is being encrypted with native RDP encryption, this is the type of cert being used.\n', - }, - { - name: 'count', - type: 'integer', - description: - 'The number of certs seen. X.509 can transfer an entire certificate chain.\n', - }, - { - name: 'permanent', - type: 'boolean', - description: - 'Indicates if the provided certificate or certificate chain is permanent or temporary.\n', - }, - ], - }, - { - name: 'encryption', - type: 'group', - fields: [ - { - name: 'level', - type: 'keyword', - description: 'Encryption level of the connection.\n', - }, - { - name: 'method', - type: 'keyword', - description: 'Encryption method of the connection.\n', - }, - ], - }, - { - name: 'done', - type: 'boolean', - description: 'Track status of logging RDP connections.\n', - }, - { - name: 'ssl', - type: 'boolean', - description: - '(present if policy/protocols/rdp/indicate_ssl.bro is loaded)\nFlag the connection if it was seen over SSL.\n', - }, - ], - }, - { - name: 'rfb', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek RFB log.\n', - fields: [ - { - name: 'version', - type: 'group', - fields: [ - { - name: 'client', - type: 'group', - fields: [ - { - name: 'major', - type: 'keyword', - description: 'Major version of the client.\n', - }, - { - name: 'minor', - type: 'keyword', - description: 'Minor version of the client.\n', - }, - ], - }, - { - name: 'server', - type: 'group', - fields: [ - { - name: 'major', - type: 'keyword', - description: 'Major version of the server.\n', - }, - { - name: 'minor', - type: 'keyword', - description: 'Minor version of the server.\n', - }, - ], - }, - ], - }, - { - name: 'auth', - type: 'group', - fields: [ - { - name: 'success', - type: 'boolean', - description: 'Whether or not authentication was successful.\n', - }, - { - name: 'method', - type: 'keyword', - description: 'Identifier of authentication method used.\n', - }, - ], - }, - { - name: 'share_flag', - type: 'boolean', - description: 'Whether the client has an exclusive or a shared session.\n', - }, - { - name: 'desktop_name', - type: 'keyword', - description: 'Name of the screen that is being shared.\n', - }, - { - name: 'width', - type: 'integer', - description: 'Width of the screen that is being shared.\n', - }, - { - name: 'height', - type: 'integer', - description: 'Height of the screen that is being shared.\n', - }, - ], - }, - { - name: 'sip', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek SIP log.\n', - fields: [ - { - name: 'transaction_depth', - type: 'integer', - description: - 'Represents the pipelined depth into the connection of this request/response transaction.\n', - }, - { - name: 'sequence', - type: 'group', - fields: [ - { - name: 'method', - type: 'keyword', - description: 'Verb used in the SIP request (INVITE, REGISTER etc.).\n', - }, - { - name: 'number', - type: 'keyword', - description: 'Contents of the CSeq: header from the client.\n', - }, - ], - }, - { - name: 'uri', - type: 'keyword', - description: 'URI used in the request.\n', - }, - { - name: 'date', - type: 'keyword', - description: 'Contents of the Date: header from the client.\n', - }, - { - name: 'request', - type: 'group', - fields: [ - { - name: 'from', - type: 'keyword', - description: - "Contents of the request From: header Note: The tag= value that's usually appended to the sender is stripped off and not logged.\n", - }, - { - name: 'to', - type: 'keyword', - description: 'Contents of the To: header.\n', - }, - { - name: 'path', - type: 'keyword', - description: - 'The client message transmission path, as extracted from the headers.\n', - }, - { - name: 'body_length', - type: 'long', - description: 'Contents of the Content-Length: header from the client.\n', - }, - ], - }, - { - name: 'response', - type: 'group', - fields: [ - { - name: 'from', - type: 'keyword', - description: - "Contents of the response From: header Note: The tag= value that's usually appended to the sender is stripped off and not logged.\n", - }, - { - name: 'to', - type: 'keyword', - description: 'Contents of the response To: header.\n', - }, - { - name: 'path', - type: 'keyword', - description: - 'The server message transmission path, as extracted from the headers.\n', - }, - { - name: 'body_length', - type: 'long', - description: 'Contents of the Content-Length: header from the server.\n', - }, - ], - }, - { - name: 'reply_to', - type: 'keyword', - description: 'Contents of the Reply-To: header.\n', - }, - { - name: 'call_id', - type: 'keyword', - description: 'Contents of the Call-ID: header from the client.\n', - }, - { - name: 'subject', - type: 'keyword', - description: 'Contents of the Subject: header from the client.\n', - }, - { - name: 'user_agent', - type: 'keyword', - description: 'Contents of the User-Agent: header from the client.\n', - }, - { - name: 'status', - type: 'group', - fields: [ - { - name: 'code', - type: 'integer', - description: 'Status code returned by the server.\n', - }, - { - name: 'msg', - type: 'keyword', - description: 'Status message returned by the server.\n', - }, - ], - }, - { - name: 'warning', - type: 'keyword', - description: 'Contents of the Warning: header.\n', - }, - { - name: 'content_type', - type: 'keyword', - description: 'Contents of the Content-Type: header from the server.\n', - }, - ], - }, - { - name: 'smb_cmd', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek smb_cmd log.\n', - fields: [ - { - name: 'command', - type: 'keyword', - description: 'The command sent by the client.\n', - }, - { - name: 'sub_command', - type: 'keyword', - description: 'The subcommand sent by the client, if present.\n', - }, - { - name: 'argument', - type: 'keyword', - description: 'Command argument sent by the client, if any.\n', - }, - { - name: 'status', - type: 'keyword', - description: "Server reply to the client's command.\n", - }, - { - name: 'rtt', - type: 'double', - description: 'Round trip time from the request to the response.\n', - }, - { - name: 'version', - type: 'keyword', - description: 'Version of SMB for the command.\n', - }, - { - name: 'username', - type: 'keyword', - description: 'Authenticated username, if available.\n', - }, - { - name: 'tree', - type: 'keyword', - description: - 'If this is related to a tree, this is the tree that was used for the current command.\n', - }, - { - name: 'tree_service', - type: 'keyword', - description: 'The type of tree (disk share, printer share, named pipe, etc.).\n', - }, - { - name: 'file', - type: 'group', - description: 'If the command referenced a file, store it here.\n', - fields: [ - { - name: 'name', - type: 'keyword', - description: 'Filename if one was seen.\n', - }, - { - name: 'action', - type: 'keyword', - description: 'Action this log record represents.\n', - }, - { - name: 'uid', - type: 'keyword', - description: 'UID of the referenced file.\n', - }, - { - name: 'host', - type: 'group', - fields: [ - { - name: 'tx', - type: 'ip', - description: 'Address of the transmitting host.\n', - }, - { - name: 'rx', - type: 'ip', - description: 'Address of the receiving host.\n', - }, - ], - }, - ], - }, - { - name: 'smb1_offered_dialects', - type: 'keyword', - description: - 'Present if base/protocols/smb/smb1-main.bro is loaded.\nDialects offered by the client.\n', - }, - { - name: 'smb2_offered_dialects', - type: 'integer', - description: - 'Present if base/protocols/smb/smb2-main.bro is loaded.\nDialects offered by the client.\n', - }, - ], - }, - { - name: 'smb_files', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek SMB Files log.\n', - fields: [ - { - name: 'action', - type: 'keyword', - description: 'Action this log record represents.\n', - }, - { - name: 'fid', - type: 'integer', - description: 'ID referencing this file.\n', - }, - { - name: 'name', - type: 'keyword', - description: 'Filename if one was seen.\n', - }, - { - name: 'path', - type: 'keyword', - description: 'Path pulled from the tree this file was transferred to or from.\n', - }, - { - name: 'previous_name', - type: 'keyword', - description: - "If the rename action was seen, this will be the file's previous name.\n", - }, - { - name: 'size', - type: 'long', - description: 'Byte size of the file.\n', - }, - { - name: 'times', - type: 'group', - description: 'Timestamps of the file.\n', - fields: [ - { - name: 'accessed', - type: 'date', - description: "The file's access time.\n", - }, - { - name: 'changed', - type: 'date', - description: "The file's change time.\n", - }, - { - name: 'created', - type: 'date', - description: "The file's create time.\n", - }, - { - name: 'modified', - type: 'date', - description: "The file's modify time.\n", - }, - ], - }, - { - name: 'uuid', - type: 'keyword', - description: 'UUID referencing this file if DCE/RPC.\n', - }, - ], - }, - { - name: 'smb_mapping', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek SMB_Mapping log.\n', - fields: [ - { - name: 'path', - type: 'keyword', - description: 'Name of the tree path.\n', - }, - { - name: 'service', - type: 'keyword', - description: - 'The type of resource of the tree (disk share, printer share, named pipe, etc.).\n', - }, - { - name: 'native_file_system', - type: 'keyword', - description: 'File system of the tree.\n', - }, - { - name: 'share_type', - type: 'keyword', - description: - 'If this is SMB2, a share type will be included. For SMB1, the type of share\nwill be deduced and included as well.\n', - }, - ], - }, - { - name: 'smtp', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek SMTP log.\n', - fields: [ - { - name: 'transaction_depth', - type: 'integer', - description: - 'A count to represent the depth of this message transaction in a single connection where multiple messages were transferred.\n', - }, - { - name: 'helo', - type: 'keyword', - description: 'Contents of the Helo header.\n', - }, - { - name: 'mail_from', - type: 'keyword', - description: 'Email addresses found in the MAIL FROM header.\n', - }, - { - name: 'rcpt_to', - type: 'keyword', - description: 'Email addresses found in the RCPT TO header.\n', - }, - { - name: 'date', - type: 'date', - description: 'Contents of the Date header.\n', - }, - { - name: 'from', - type: 'keyword', - description: 'Contents of the From header.\n', - }, - { - name: 'to', - type: 'keyword', - description: 'Contents of the To header.\n', - }, - { - name: 'cc', - type: 'keyword', - description: 'Contents of the CC header.\n', - }, - { - name: 'reply_to', - type: 'keyword', - description: 'Contents of the ReplyTo header.\n', - }, - { - name: 'msg_id', - type: 'keyword', - description: 'Contents of the MsgID header.\n', - }, - { - name: 'in_reply_to', - type: 'keyword', - description: 'Contents of the In-Reply-To header.\n', - }, - { - name: 'subject', - type: 'keyword', - description: 'Contents of the Subject header.\n', - }, - { - name: 'x_originating_ip', - type: 'keyword', - description: 'Contents of the X-Originating-IP header.\n', - }, - { - name: 'first_received', - type: 'keyword', - description: 'Contents of the first Received header.\n', - }, - { - name: 'second_received', - type: 'keyword', - description: 'Contents of the second Received header.\n', - }, - { - name: 'last_reply', - type: 'keyword', - description: 'The last message that the server sent to the client.\n', - }, - { - name: 'path', - type: 'ip', - description: 'The message transmission path, as extracted from the headers.\n', - }, - { - name: 'user_agent', - type: 'keyword', - description: 'Value of the User-Agent header from the client.\n', - }, - { - name: 'tls', - type: 'boolean', - description: 'Indicates that the connection has switched to using TLS.\n', - }, - { - name: 'process_received_from', - type: 'boolean', - description: - 'Indicates if the "Received: from" headers should still be processed.\n', - }, - { - name: 'has_client_activity', - type: 'boolean', - description: 'Indicates if client activity has been seen, but not yet logged.\n', - }, - { - name: 'fuids', - type: 'keyword', - description: - '(present if base/protocols/smtp/files.bro is loaded)\nAn ordered vector of file unique IDs seen attached to the message.\n', - }, - { - name: 'is_webmail', - type: 'boolean', - description: 'Indicates if the message was sent through a webmail interface.\n', - }, - ], - }, - { - name: 'snmp', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek SNMP log.\n', - fields: [ - { - name: 'duration', - type: 'double', - description: - 'The amount of time between the first packet beloning to the SNMP session and the latest one seen.\n', - }, - { - name: 'version', - type: 'keyword', - description: 'The version of SNMP being used.\n', - }, - { - name: 'community', - type: 'keyword', - description: - "The community string of the first SNMP packet associated with the session. This is used as part of SNMP's (v1 and v2c) administrative/security framework. See RFC 1157 or RFC 1901.\n", - }, - { - name: 'get', - type: 'group', - fields: [ - { - name: 'requests', - type: 'integer', - description: - 'The number of variable bindings in GetRequest/GetNextRequest PDUs seen for the session.\n', - }, - { - name: 'bulk_requests', - type: 'integer', - description: - 'The number of variable bindings in GetBulkRequest PDUs seen for the session.\n', - }, - { - name: 'responses', - type: 'integer', - description: - 'The number of variable bindings in GetResponse/Response PDUs seen for the session.\n', - }, - ], - }, - { - name: 'set', - type: 'group', - fields: [ - { - name: 'requests', - type: 'integer', - description: - 'The number of variable bindings in SetRequest PDUs seen for the session.\n', - }, - ], - }, - { - name: 'display_string', - type: 'keyword', - description: 'A system description of the SNMP responder endpoint.\n', - }, - { - name: 'up_since', - type: 'date', - description: - "The time at which the SNMP responder endpoint claims it's been up since.\n", - }, - ], - }, - { - name: 'socks', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek SOCKS log.\n', - fields: [ - { - name: 'version', - type: 'integer', - description: 'Protocol version of SOCKS.\n', - }, - { - name: 'user', - type: 'keyword', - description: 'Username used to request a login to the proxy.\n', - }, - { - name: 'password', - type: 'keyword', - description: 'Password used to request a login to the proxy.\n', - }, - { - name: 'status', - type: 'keyword', - description: 'Server status for the attempt at using the proxy.\n', - }, - { - name: 'request', - type: 'group', - fields: [ - { - name: 'host', - type: 'keyword', - description: - 'Client requested SOCKS address. Could be an address, a name or both.\n', - }, - { - name: 'port', - type: 'integer', - description: 'Client requested port.\n', - }, - ], - }, - { - name: 'bound', - type: 'group', - fields: [ - { - name: 'host', - type: 'keyword', - description: 'Server bound address. Could be an address, a name or both.\n', - }, - { - name: 'port', - type: 'integer', - description: 'Server bound port.\n', - }, - ], - }, - { - name: 'capture_password', - type: 'boolean', - description: 'Determines if the password will be captured for this request.\n', - }, - ], - }, - { - name: 'ssh', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek SSH log.\n', - fields: [ - { - name: 'client', - type: 'keyword', - description: "The client's version string.\n", - }, - { - name: 'direction', - type: 'keyword', - description: - 'Direction of the connection. If the client was a local host logging into\nan external host, this would be OUTBOUND. INBOUND would be set for the\nopposite situation.\n', - }, - { - name: 'host_key', - type: 'keyword', - description: "The server's key thumbprint.\n", - }, - { - name: 'server', - type: 'keyword', - description: "The server's version string.\n", - }, - { - name: 'version', - type: 'integer', - description: 'SSH major version (1 or 2).\n', - }, - { - name: 'algorithm', - type: 'group', - description: 'Cipher algorithms used in this session.\n', - fields: [ - { - name: 'cipher', - type: 'keyword', - description: 'The encryption algorithm in use.\n', - }, - { - name: 'compression', - type: 'keyword', - description: 'The compression algorithm in use.\n', - }, - { - name: 'host_key', - type: 'keyword', - description: "The server host key's algorithm.\n", - }, - { - name: 'key_exchange', - type: 'keyword', - description: 'The key exchange algorithm in use.\n', - }, - { - name: 'mac', - type: 'keyword', - description: 'The signing (MAC) algorithm in use.\n', - }, - ], - }, - { - name: 'auth', - type: 'group', - fields: [ - { - name: 'attempts', - type: 'integer', - description: - "The number of authentication attemps we observed. There's always at\nleast one, since some servers might support no authentication at all.\nIt's important to note that not all of these are failures, since some\nservers require two-factor auth (e.g. password AND pubkey).\n", - }, - { - name: 'success', - type: 'boolean', - description: 'Authentication result.\n', - }, - ], - }, - ], - }, - { - name: 'ssl', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek SSL log.\n', - fields: [ - { - name: 'version', - type: 'keyword', - description: 'SSL/TLS version that was logged.\n', - }, - { - name: 'cipher', - type: 'keyword', - description: 'SSL/TLS cipher suite that was logged.\n', - }, - { - name: 'curve', - type: 'keyword', - description: 'Elliptic curve that was logged when using ECDH/ECDHE.\n', - }, - { - name: 'resumed', - type: 'boolean', - description: - 'Flag to indicate if the session was resumed reusing the key material exchanged in an\nearlier connection.\n', - }, - { - name: 'next_protocol', - type: 'keyword', - description: - 'Next protocol the server chose using the application layer next protocol extension.\n', - }, - { - name: 'established', - type: 'boolean', - description: - 'Flag to indicate if this ssl session has been established successfully.\n', - }, - { - name: 'validation', - type: 'group', - fields: [ - { - name: 'status', - type: 'keyword', - description: 'Result of certificate validation for this connection.\n', - }, - { - name: 'code', - type: 'keyword', - description: - 'Result of certificate validation for this connection, given as OpenSSL validation code.\n', - }, - ], - }, - { - name: 'last_alert', - type: 'keyword', - description: 'Last alert that was seen during the connection.\n', - }, - { - name: 'server', - type: 'group', - fields: [ - { - name: 'name', - type: 'keyword', - description: - 'Value of the Server Name Indicator SSL/TLS extension. It indicates the server name\nthat the client was requesting.\n', - }, - { - name: 'cert_chain', - type: 'keyword', - description: - 'Chain of certificates offered by the server to validate its complete signing chain.\n', - }, - { - name: 'cert_chain_fuids', - type: 'keyword', - description: - 'An ordered vector of certificate file identifiers for the certificates offered by the server.\n', - }, - { - name: 'issuer', - type: 'group', - description: - 'Subject of the signer of the X.509 certificate offered by the server.\n', - fields: [ - { - name: 'common_name', - type: 'keyword', - description: - 'Common name of the signer of the X.509 certificate offered by the server.\n', - }, - { - name: 'country', - type: 'keyword', - description: - 'Country code of the signer of the X.509 certificate offered by the server.\n', - }, - { - name: 'locality', - type: 'keyword', - description: - 'Locality of the signer of the X.509 certificate offered by the server.\n', - }, - { - name: 'organization', - type: 'keyword', - description: - 'Organization of the signer of the X.509 certificate offered by the server.\n', - }, - { - name: 'organizational_unit', - type: 'keyword', - description: - 'Organizational unit of the signer of the X.509 certificate offered by the server.\n', - }, - { - name: 'state', - type: 'keyword', - description: - 'State or province name of the signer of the X.509 certificate offered by the server.\n', - }, - ], - }, - { - name: 'subject', - type: 'group', - description: 'Subject of the X.509 certificate offered by the server.\n', - fields: [ - { - name: 'common_name', - type: 'keyword', - description: - 'Common name of the X.509 certificate offered by the server.\n', - }, - { - name: 'country', - type: 'keyword', - description: - 'Country code of the X.509 certificate offered by the server.\n', - }, - { - name: 'locality', - type: 'keyword', - description: 'Locality of the X.509 certificate offered by the server.\n', - }, - { - name: 'organization', - type: 'keyword', - description: - 'Organization of the X.509 certificate offered by the server.\n', - }, - { - name: 'organizational_unit', - type: 'keyword', - description: - 'Organizational unit of the X.509 certificate offered by the server.\n', - }, - { - name: 'state', - type: 'keyword', - description: - 'State or province name of the X.509 certificate offered by the server.\n', - }, - ], - }, - ], - }, - { - name: 'client', - type: 'group', - fields: [ - { - name: 'cert_chain', - type: 'keyword', - description: - 'Chain of certificates offered by the client to validate its complete signing chain.\n', - }, - { - name: 'cert_chain_fuids', - type: 'keyword', - description: - 'An ordered vector of certificate file identifiers for the certificates offered by the client.\n', - }, - { - name: 'issuer', - type: 'group', - description: - 'Subject of the signer of the X.509 certificate offered by the client.\n', - fields: [ - { - name: 'common_name', - type: 'keyword', - description: - 'Common name of the signer of the X.509 certificate offered by the client.\n', - }, - { - name: 'country', - type: 'keyword', - description: - 'Country code of the signer of the X.509 certificate offered by the client.\n', - }, - { - name: 'locality', - type: 'keyword', - description: - 'Locality of the signer of the X.509 certificate offered by the client.\n', - }, - { - name: 'organization', - type: 'keyword', - description: - 'Organization of the signer of the X.509 certificate offered by the client.\n', - }, - { - name: 'organizational_unit', - type: 'keyword', - description: - 'Organizational unit of the signer of the X.509 certificate offered by the client.\n', - }, - { - name: 'state', - type: 'keyword', - description: - 'State or province name of the signer of the X.509 certificate offered by the client.\n', - }, - ], - }, - { - name: 'subject', - type: 'group', - description: 'Subject of the X.509 certificate offered by the client.\n', - fields: [ - { - name: 'common_name', - type: 'keyword', - description: - 'Common name of the X.509 certificate offered by the client.\n', - }, - { - name: 'country', - type: 'keyword', - description: - 'Country code of the X.509 certificate offered by the client.\n', - }, - { - name: 'locality', - type: 'keyword', - description: 'Locality of the X.509 certificate offered by the client.\n', - }, - { - name: 'organization', - type: 'keyword', - description: - 'Organization of the X.509 certificate offered by the client.\n', - }, - { - name: 'organizational_unit', - type: 'keyword', - description: - 'Organizational unit of the X.509 certificate offered by the client.\n', - }, - { - name: 'state', - type: 'keyword', - description: - 'State or province name of the X.509 certificate offered by the client.\n', - }, - ], - }, - ], - }, - ], - }, - { - name: 'stats', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek stats log.\n', - fields: [ - { - name: 'peer', - type: 'keyword', - description: 'Peer that generated this log. Mostly for clusters.\n', - }, - { - name: 'memory', - type: 'integer', - description: 'Amount of memory currently in use in MB.\n', - }, - { - name: 'packets', - type: 'group', - fields: [ - { - name: 'processed', - type: 'long', - description: 'Number of packets processed since the last stats interval.\n', - }, - { - name: 'dropped', - type: 'long', - description: - 'Number of packets dropped since the last stats interval if reading live traffic.\n', - }, - { - name: 'received', - type: 'long', - description: - 'Number of packets seen on the link since the last stats interval if reading live traffic.\n', - }, - ], - }, - { - name: 'bytes', - type: 'group', - fields: [ - { - name: 'received', - type: 'long', - description: - 'Number of bytes received since the last stats interval if reading live traffic.\n', - }, - ], - }, - { - name: 'connections', - type: 'group', - fields: [ - { - name: 'tcp', - type: 'group', - fields: [ - { - name: 'active', - type: 'integer', - description: 'TCP connections currently in memory.\n', - }, - { - name: 'count', - type: 'integer', - description: 'TCP connections seen since last stats interval.\n', - }, - ], - }, - { - name: 'udp', - type: 'group', - fields: [ - { - name: 'active', - type: 'integer', - description: 'UDP connections currently in memory.\n', - }, - { - name: 'count', - type: 'integer', - description: 'UDP connections seen since last stats interval.\n', - }, - ], - }, - { - name: 'icmp', - type: 'group', - fields: [ - { - name: 'active', - type: 'integer', - description: 'ICMP connections currently in memory.\n', - }, - { - name: 'count', - type: 'integer', - description: 'ICMP connections seen since last stats interval.\n', - }, - ], - }, - ], - }, - { - name: 'events', - type: 'group', - fields: [ - { - name: 'processed', - type: 'integer', - description: 'Number of events processed since the last stats interval.\n', - }, - { - name: 'queued', - type: 'integer', - description: - 'Number of events that have been queued since the last stats interval.\n', - }, - ], - }, - { - name: 'timers', - type: 'group', - fields: [ - { - name: 'count', - type: 'integer', - description: 'Number of timers scheduled since last stats interval.\n', - }, - { - name: 'active', - type: 'integer', - description: 'Current number of scheduled timers.\n', - }, - ], - }, - { - name: 'files', - type: 'group', - fields: [ - { - name: 'count', - type: 'integer', - description: 'Number of files seen since last stats interval.\n', - }, - { - name: 'active', - type: 'integer', - description: 'Current number of files actively being seen.\n', - }, - ], - }, - { - name: 'dns_requests', - type: 'group', - fields: [ - { - name: 'count', - type: 'integer', - description: 'Number of DNS requests seen since last stats interval.\n', - }, - { - name: 'active', - type: 'integer', - description: 'Current number of DNS requests awaiting a reply.\n', - }, - ], - }, - { - name: 'reassembly_size', - type: 'group', - fields: [ - { - name: 'tcp', - type: 'integer', - description: 'Current size of TCP data in reassembly.\n', - }, - { - name: 'file', - type: 'integer', - description: 'Current size of File data in reassembly.\n', - }, - { - name: 'frag', - type: 'integer', - description: 'Current size of packet fragment data in reassembly.\n', - }, - { - name: 'unknown', - type: 'integer', - description: - 'Current size of unknown data in reassembly (this is only PIA buffer right now).\n', - }, - ], - }, - { - name: 'timestamp_lag', - type: 'integer', - description: - 'Lag between the wall clock and packet timestamps if reading live traffic.\n', - }, - ], - }, - { - name: 'syslog', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek syslog log.\n', - fields: [ - { - name: 'facility', - type: 'keyword', - description: 'Syslog facility for the message.\n', - }, - { - name: 'severity', - type: 'keyword', - description: 'Syslog severity for the message.\n', - }, - { - name: 'message', - type: 'keyword', - description: 'The plain text message.\n', - }, - ], - }, - { - name: 'tunnel', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek SSH log.\n', - fields: [ - { - name: 'type', - type: 'keyword', - description: 'The type of tunnel.\n', - }, - { - name: 'action', - type: 'keyword', - description: 'The type of activity that occurred.\n', - }, - ], - }, - { - name: 'weird', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek Weird log.\n', - fields: [ - { - name: 'name', - type: 'keyword', - description: 'The name of the weird that occurred.\n', - }, - { - name: 'additional_info', - type: 'keyword', - description: 'Additional information accompanying the weird if any.\n', - }, - { - name: 'notice', - type: 'boolean', - description: 'Indicate if this weird was also turned into a notice.\n', - }, - { - name: 'peer', - type: 'keyword', - description: - 'The peer that originated this weird. This is helpful in cluster deployments if a particular cluster node is having trouble to help identify which node is having trouble.\n', - }, - { - name: 'identifier', - type: 'keyword', - description: - 'This field is to be provided when a weird is generated for the purpose of deduplicating weirds. The identifier string should be unique for a single instance of the weird. This field is used to define when a weird is conceptually a duplicate of a previous weird.\n', - }, - ], - }, - { - name: 'x509', - type: 'group', - default_field: false, - description: 'Fields exported by the Zeek x509 log.\n', - fields: [ - { - name: 'id', - type: 'keyword', - description: 'File id of this certificate.\n', - }, - { - name: 'certificate', - type: 'group', - description: 'Basic information about the certificate.\n', - fields: [ - { - name: 'version', - type: 'integer', - description: 'Version number.\n', - }, - { - name: 'serial', - type: 'keyword', - description: 'Serial number.\n', - }, - { - name: 'subject', - type: 'group', - description: 'Subject.\n', - fields: [ - { - name: 'country', - type: 'keyword', - description: 'Country provided in the certificate subject.\n', - }, - { - name: 'common_name', - type: 'keyword', - description: 'Common name provided in the certificate subject.\n', - }, - { - name: 'locality', - type: 'keyword', - description: 'Locality provided in the certificate subject.\n', - }, - { - name: 'organization', - type: 'keyword', - description: 'Organization provided in the certificate subject.\n', - }, - { - name: 'organizational_unit', - type: 'keyword', - description: 'Organizational unit provided in the certificate subject.\n', - }, - { - name: 'state', - type: 'keyword', - description: 'State or province provided in the certificate subject.\n', - }, - ], - }, - { - name: 'issuer', - type: 'group', - description: 'Issuer.\n', - fields: [ - { - name: 'country', - type: 'keyword', - description: 'Country provided in the certificate issuer field.\n', - }, - { - name: 'common_name', - type: 'keyword', - description: 'Common name provided in the certificate issuer field.\n', - }, - { - name: 'locality', - type: 'keyword', - description: 'Locality provided in the certificate issuer field.\n', - }, - { - name: 'organization', - type: 'keyword', - description: 'Organization provided in the certificate issuer field.\n', - }, - { - name: 'organizational_unit', - type: 'keyword', - description: - 'Organizational unit provided in the certificate issuer field.\n', - }, - { - name: 'state', - type: 'keyword', - description: - 'State or province provided in the certificate issuer field.\n', - }, - ], - }, - { - name: 'common_name', - type: 'keyword', - description: 'Last (most specific) common name.\n', - }, - { - name: 'valid', - type: 'group', - description: 'Certificate validity timestamps\n', - fields: [ - { - name: 'from', - type: 'date', - description: 'Timestamp before when certificate is not valid.\n', - }, - { - name: 'until', - type: 'date', - description: 'Timestamp after when certificate is not valid.\n', - }, - ], - }, - { - name: 'key', - type: 'group', - fields: [ - { - name: 'algorithm', - type: 'keyword', - description: 'Name of the key algorithm.\n', - }, - { - name: 'type', - type: 'keyword', - description: - 'Key type, if key parseable by openssl (either rsa, dsa or ec).\n', - }, - { - name: 'length', - type: 'integer', - description: 'Key length in bits.\n', - }, - ], - }, - { - name: 'signature_algorithm', - type: 'keyword', - description: 'Name of the signature algorithm.\n', - }, - { - name: 'exponent', - type: 'keyword', - description: 'Exponent, if RSA-certificate.\n', - }, - { - name: 'curve', - type: 'keyword', - description: 'Curve, if EC-certificate.\n', - }, - ], - }, - { - name: 'san', - type: 'group', - description: 'Subject alternative name extension of the certificate.\n', - fields: [ - { - name: 'dns', - type: 'keyword', - description: 'List of DNS entries in SAN.\n', - }, - { - name: 'uri', - type: 'keyword', - description: 'List of URI entries in SAN.\n', - }, - { - name: 'email', - type: 'keyword', - description: 'List of email entries in SAN.\n', - }, - { - name: 'ip', - type: 'ip', - description: 'List of IP entries in SAN.\n', - }, - { - name: 'other_fields', - type: 'boolean', - description: - 'True if the certificate contained other, not recognized or parsed name fields.\n', - }, - ], - }, - { - name: 'basic_constraints', - type: 'group', - description: 'Basic constraints extension of the certificate.\n', - fields: [ - { - name: 'certificate_authority', - type: 'boolean', - description: 'CA flag set or not.\n', - }, - { - name: 'path_length', - type: 'integer', - description: 'Maximum path length.\n', - }, - ], - }, - { - name: 'log_cert', - type: 'boolean', - description: - 'Present if policy/protocols/ssl/log-hostcerts-only.bro is loaded\nLogging of certificate is suppressed if set to F.\n', - }, - ], - }, - ], - }, - ], - }, - { - key: 'netflow', - title: 'NetFlow', - description: 'Fields from NetFlow and IPFIX flows.\n', - fields: [ - { - name: 'netflow', - type: 'group', - description: 'Fields from NetFlow and IPFIX.\n', - fields: [ - { - name: 'type', - type: 'keyword', - description: 'The type of NetFlow record described by this event.\n', - }, - { - name: 'exporter', - type: 'group', - description: 'Metadata related to the exporter device that generated this record.\n', - fields: [ - { - name: 'address', - type: 'keyword', - description: "Exporter's network address in IP:port format.\n", - }, - { - name: 'source_id', - type: 'long', - description: 'Observation domain ID to which this record belongs.\n', - }, - { - name: 'timestamp', - type: 'date', - description: 'Time and date of export.\n', - }, - { - name: 'uptime_millis', - type: 'long', - description: 'How long the exporter process has been running, in milliseconds.\n', - }, - { - name: 'version', - type: 'integer', - description: 'NetFlow version used.\n', - }, - ], - }, - { - name: 'octet_delta_count', - type: 'long', - }, - { - name: 'packet_delta_count', - type: 'long', - }, - { - name: 'delta_flow_count', - type: 'long', - }, - { - name: 'protocol_identifier', - type: 'short', - }, - { - name: 'ip_class_of_service', - type: 'short', - }, - { - name: 'tcp_control_bits', - type: 'integer', - }, - { - name: 'source_transport_port', - type: 'integer', - }, - { - name: 'source_ipv4_address', - type: 'ip', - }, - { - name: 'source_ipv4_prefix_length', - type: 'short', - }, - { - name: 'ingress_interface', - type: 'long', - }, - { - name: 'destination_transport_port', - type: 'integer', - }, - { - name: 'destination_ipv4_address', - type: 'ip', - }, - { - name: 'destination_ipv4_prefix_length', - type: 'short', - }, - { - name: 'egress_interface', - type: 'long', - }, - { - name: 'ip_next_hop_ipv4_address', - type: 'ip', - }, - { - name: 'bgp_source_as_number', - type: 'long', - }, - { - name: 'bgp_destination_as_number', - type: 'long', - }, - { - name: 'bgp_next_hop_ipv4_address', - type: 'ip', - }, - { - name: 'post_mcast_packet_delta_count', - type: 'long', - }, - { - name: 'post_mcast_octet_delta_count', - type: 'long', - }, - { - name: 'flow_end_sys_up_time', - type: 'long', - }, - { - name: 'flow_start_sys_up_time', - type: 'long', - }, - { - name: 'post_octet_delta_count', - type: 'long', - }, - { - name: 'post_packet_delta_count', - type: 'long', - }, - { - name: 'minimum_ip_total_length', - type: 'long', - }, - { - name: 'maximum_ip_total_length', - type: 'long', - }, - { - name: 'source_ipv6_address', - type: 'ip', - }, - { - name: 'destination_ipv6_address', - type: 'ip', - }, - { - name: 'source_ipv6_prefix_length', - type: 'short', - }, - { - name: 'destination_ipv6_prefix_length', - type: 'short', - }, - { - name: 'flow_label_ipv6', - type: 'long', - }, - { - name: 'icmp_type_code_ipv4', - type: 'integer', - }, - { - name: 'igmp_type', - type: 'short', - }, - { - name: 'sampling_interval', - type: 'long', - }, - { - name: 'sampling_algorithm', - type: 'short', - }, - { - name: 'flow_active_timeout', - type: 'integer', - }, - { - name: 'flow_idle_timeout', - type: 'integer', - }, - { - name: 'engine_type', - type: 'short', - }, - { - name: 'engine_id', - type: 'short', - }, - { - name: 'exported_octet_total_count', - type: 'long', - }, - { - name: 'exported_message_total_count', - type: 'long', - }, - { - name: 'exported_flow_record_total_count', - type: 'long', - }, - { - name: 'ipv4_router_sc', - type: 'ip', - }, - { - name: 'source_ipv4_prefix', - type: 'ip', - }, - { - name: 'destination_ipv4_prefix', - type: 'ip', - }, - { - name: 'mpls_top_label_type', - type: 'short', - }, - { - name: 'mpls_top_label_ipv4_address', - type: 'ip', - }, - { - name: 'sampler_id', - type: 'short', - }, - { - name: 'sampler_mode', - type: 'short', - }, - { - name: 'sampler_random_interval', - type: 'long', - }, - { - name: 'class_id', - type: 'long', - }, - { - name: 'minimum_ttl', - type: 'short', - }, - { - name: 'maximum_ttl', - type: 'short', - }, - { - name: 'fragment_identification', - type: 'long', - }, - { - name: 'post_ip_class_of_service', - type: 'short', - }, - { - name: 'source_mac_address', - type: 'keyword', - }, - { - name: 'post_destination_mac_address', - type: 'keyword', - }, - { - name: 'vlan_id', - type: 'integer', - }, - { - name: 'post_vlan_id', - type: 'integer', - }, - { - name: 'ip_version', - type: 'short', - }, - { - name: 'flow_direction', - type: 'short', - }, - { - name: 'ip_next_hop_ipv6_address', - type: 'ip', - }, - { - name: 'bgp_next_hop_ipv6_address', - type: 'ip', - }, - { - name: 'ipv6_extension_headers', - type: 'long', - }, - { - name: 'mpls_top_label_stack_section', - type: 'short', - }, - { - name: 'mpls_label_stack_section2', - type: 'short', - }, - { - name: 'mpls_label_stack_section3', - type: 'short', - }, - { - name: 'mpls_label_stack_section4', - type: 'short', - }, - { - name: 'mpls_label_stack_section5', - type: 'short', - }, - { - name: 'mpls_label_stack_section6', - type: 'short', - }, - { - name: 'mpls_label_stack_section7', - type: 'short', - }, - { - name: 'mpls_label_stack_section8', - type: 'short', - }, - { - name: 'mpls_label_stack_section9', - type: 'short', - }, - { - name: 'mpls_label_stack_section10', - type: 'short', - }, - { - name: 'destination_mac_address', - type: 'keyword', - }, - { - name: 'post_source_mac_address', - type: 'keyword', - }, - { - name: 'interface_name', - type: 'keyword', - }, - { - name: 'interface_description', - type: 'keyword', - }, - { - name: 'sampler_name', - type: 'keyword', - }, - { - name: 'octet_total_count', - type: 'long', - }, - { - name: 'packet_total_count', - type: 'long', - }, - { - name: 'flags_and_sampler_id', - type: 'long', - }, - { - name: 'fragment_offset', - type: 'integer', - }, - { - name: 'forwarding_status', - type: 'short', - }, - { - name: 'mpls_vpn_route_distinguisher', - type: 'short', - }, - { - name: 'mpls_top_label_prefix_length', - type: 'short', - }, - { - name: 'src_traffic_index', - type: 'long', - }, - { - name: 'dst_traffic_index', - type: 'long', - }, - { - name: 'application_description', - type: 'keyword', - }, - { - name: 'application_id', - type: 'short', - }, - { - name: 'application_name', - type: 'keyword', - }, - { - name: 'post_ip_diff_serv_code_point', - type: 'short', - }, - { - name: 'multicast_replication_factor', - type: 'long', - }, - { - name: 'class_name', - type: 'keyword', - }, - { - name: 'classification_engine_id', - type: 'short', - }, - { - name: 'layer2packet_section_offset', - type: 'integer', - }, - { - name: 'layer2packet_section_size', - type: 'integer', - }, - { - name: 'layer2packet_section_data', - type: 'short', - }, - { - name: 'bgp_next_adjacent_as_number', - type: 'long', - }, - { - name: 'bgp_prev_adjacent_as_number', - type: 'long', - }, - { - name: 'exporter_ipv4_address', - type: 'ip', - }, - { - name: 'exporter_ipv6_address', - type: 'ip', - }, - { - name: 'dropped_octet_delta_count', - type: 'long', - }, - { - name: 'dropped_packet_delta_count', - type: 'long', - }, - { - name: 'dropped_octet_total_count', - type: 'long', - }, - { - name: 'dropped_packet_total_count', - type: 'long', - }, - { - name: 'flow_end_reason', - type: 'short', - }, - { - name: 'common_properties_id', - type: 'long', - }, - { - name: 'observation_point_id', - type: 'long', - }, - { - name: 'icmp_type_code_ipv6', - type: 'integer', - }, - { - name: 'mpls_top_label_ipv6_address', - type: 'ip', - }, - { - name: 'line_card_id', - type: 'long', - }, - { - name: 'port_id', - type: 'long', - }, - { - name: 'metering_process_id', - type: 'long', - }, - { - name: 'exporting_process_id', - type: 'long', - }, - { - name: 'template_id', - type: 'integer', - }, - { - name: 'wlan_channel_id', - type: 'short', - }, - { - name: 'wlan_ssid', - type: 'keyword', - }, - { - name: 'flow_id', - type: 'long', - }, - { - name: 'observation_domain_id', - type: 'long', - }, - { - name: 'flow_start_seconds', - type: 'date', - }, - { - name: 'flow_end_seconds', - type: 'date', - }, - { - name: 'flow_start_milliseconds', - type: 'date', - }, - { - name: 'flow_end_milliseconds', - type: 'date', - }, - { - name: 'flow_start_microseconds', - type: 'date', - }, - { - name: 'flow_end_microseconds', - type: 'date', - }, - { - name: 'flow_start_nanoseconds', - type: 'date', - }, - { - name: 'flow_end_nanoseconds', - type: 'date', - }, - { - name: 'flow_start_delta_microseconds', - type: 'long', - }, - { - name: 'flow_end_delta_microseconds', - type: 'long', - }, - { - name: 'system_init_time_milliseconds', - type: 'date', - }, - { - name: 'flow_duration_milliseconds', - type: 'long', - }, - { - name: 'flow_duration_microseconds', - type: 'long', - }, - { - name: 'observed_flow_total_count', - type: 'long', - }, - { - name: 'ignored_packet_total_count', - type: 'long', - }, - { - name: 'ignored_octet_total_count', - type: 'long', - }, - { - name: 'not_sent_flow_total_count', - type: 'long', - }, - { - name: 'not_sent_packet_total_count', - type: 'long', - }, - { - name: 'not_sent_octet_total_count', - type: 'long', - }, - { - name: 'destination_ipv6_prefix', - type: 'ip', - }, - { - name: 'source_ipv6_prefix', - type: 'ip', - }, - { - name: 'post_octet_total_count', - type: 'long', - }, - { - name: 'post_packet_total_count', - type: 'long', - }, - { - name: 'flow_key_indicator', - type: 'long', - }, - { - name: 'post_mcast_packet_total_count', - type: 'long', - }, - { - name: 'post_mcast_octet_total_count', - type: 'long', - }, - { - name: 'icmp_type_ipv4', - type: 'short', - }, - { - name: 'icmp_code_ipv4', - type: 'short', - }, - { - name: 'icmp_type_ipv6', - type: 'short', - }, - { - name: 'icmp_code_ipv6', - type: 'short', - }, - { - name: 'udp_source_port', - type: 'integer', - }, - { - name: 'udp_destination_port', - type: 'integer', - }, - { - name: 'tcp_source_port', - type: 'integer', - }, - { - name: 'tcp_destination_port', - type: 'integer', - }, - { - name: 'tcp_sequence_number', - type: 'long', - }, - { - name: 'tcp_acknowledgement_number', - type: 'long', - }, - { - name: 'tcp_window_size', - type: 'integer', - }, - { - name: 'tcp_urgent_pointer', - type: 'integer', - }, - { - name: 'tcp_header_length', - type: 'short', - }, - { - name: 'ip_header_length', - type: 'short', - }, - { - name: 'total_length_ipv4', - type: 'integer', - }, - { - name: 'payload_length_ipv6', - type: 'integer', - }, - { - name: 'ip_ttl', - type: 'short', - }, - { - name: 'next_header_ipv6', - type: 'short', - }, - { - name: 'mpls_payload_length', - type: 'long', - }, - { - name: 'ip_diff_serv_code_point', - type: 'short', - }, - { - name: 'ip_precedence', - type: 'short', - }, - { - name: 'fragment_flags', - type: 'short', - }, - { - name: 'octet_delta_sum_of_squares', - type: 'long', - }, - { - name: 'octet_total_sum_of_squares', - type: 'long', - }, - { - name: 'mpls_top_label_ttl', - type: 'short', - }, - { - name: 'mpls_label_stack_length', - type: 'long', - }, - { - name: 'mpls_label_stack_depth', - type: 'long', - }, - { - name: 'mpls_top_label_exp', - type: 'short', - }, - { - name: 'ip_payload_length', - type: 'long', - }, - { - name: 'udp_message_length', - type: 'integer', - }, - { - name: 'is_multicast', - type: 'short', - }, - { - name: 'ipv4_ihl', - type: 'short', - }, - { - name: 'ipv4_options', - type: 'long', - }, - { - name: 'tcp_options', - type: 'long', - }, - { - name: 'padding_octets', - type: 'short', - }, - { - name: 'collector_ipv4_address', - type: 'ip', - }, - { - name: 'collector_ipv6_address', - type: 'ip', - }, - { - name: 'export_interface', - type: 'long', - }, - { - name: 'export_protocol_version', - type: 'short', - }, - { - name: 'export_transport_protocol', - type: 'short', - }, - { - name: 'collector_transport_port', - type: 'integer', - }, - { - name: 'exporter_transport_port', - type: 'integer', - }, - { - name: 'tcp_syn_total_count', - type: 'long', - }, - { - name: 'tcp_fin_total_count', - type: 'long', - }, - { - name: 'tcp_rst_total_count', - type: 'long', - }, - { - name: 'tcp_psh_total_count', - type: 'long', - }, - { - name: 'tcp_ack_total_count', - type: 'long', - }, - { - name: 'tcp_urg_total_count', - type: 'long', - }, - { - name: 'ip_total_length', - type: 'long', - }, - { - name: 'post_nat_source_ipv4_address', - type: 'ip', - }, - { - name: 'post_nat_destination_ipv4_address', - type: 'ip', - }, - { - name: 'post_napt_source_transport_port', - type: 'integer', - }, - { - name: 'post_napt_destination_transport_port', - type: 'integer', - }, - { - name: 'nat_originating_address_realm', - type: 'short', - }, - { - name: 'nat_event', - type: 'short', - }, - { - name: 'initiator_octets', - type: 'long', - }, - { - name: 'responder_octets', - type: 'long', - }, - { - name: 'firewall_event', - type: 'short', - }, - { - name: 'ingress_vrfid', - type: 'long', - }, - { - name: 'egress_vrfid', - type: 'long', - }, - { - name: 'vr_fname', - type: 'keyword', - }, - { - name: 'post_mpls_top_label_exp', - type: 'short', - }, - { - name: 'tcp_window_scale', - type: 'integer', - }, - { - name: 'biflow_direction', - type: 'short', - }, - { - name: 'ethernet_header_length', - type: 'short', - }, - { - name: 'ethernet_payload_length', - type: 'integer', - }, - { - name: 'ethernet_total_length', - type: 'integer', - }, - { - name: 'dot1q_vlan_id', - type: 'integer', - }, - { - name: 'dot1q_priority', - type: 'short', - }, - { - name: 'dot1q_customer_vlan_id', - type: 'integer', - }, - { - name: 'dot1q_customer_priority', - type: 'short', - }, - { - name: 'metro_evc_id', - type: 'keyword', - }, - { - name: 'metro_evc_type', - type: 'short', - }, - { - name: 'pseudo_wire_id', - type: 'long', - }, - { - name: 'pseudo_wire_type', - type: 'integer', - }, - { - name: 'pseudo_wire_control_word', - type: 'long', - }, - { - name: 'ingress_physical_interface', - type: 'long', - }, - { - name: 'egress_physical_interface', - type: 'long', - }, - { - name: 'post_dot1q_vlan_id', - type: 'integer', - }, - { - name: 'post_dot1q_customer_vlan_id', - type: 'integer', - }, - { - name: 'ethernet_type', - type: 'integer', - }, - { - name: 'post_ip_precedence', - type: 'short', - }, - { - name: 'collection_time_milliseconds', - type: 'date', - }, - { - name: 'export_sctp_stream_id', - type: 'integer', - }, - { - name: 'max_export_seconds', - type: 'date', - }, - { - name: 'max_flow_end_seconds', - type: 'date', - }, - { - name: 'message_md5_checksum', - type: 'short', - }, - { - name: 'message_scope', - type: 'short', - }, - { - name: 'min_export_seconds', - type: 'date', - }, - { - name: 'min_flow_start_seconds', - type: 'date', - }, - { - name: 'opaque_octets', - type: 'short', - }, - { - name: 'session_scope', - type: 'short', - }, - { - name: 'max_flow_end_microseconds', - type: 'date', - }, - { - name: 'max_flow_end_milliseconds', - type: 'date', - }, - { - name: 'max_flow_end_nanoseconds', - type: 'date', - }, - { - name: 'min_flow_start_microseconds', - type: 'date', - }, - { - name: 'min_flow_start_milliseconds', - type: 'date', - }, - { - name: 'min_flow_start_nanoseconds', - type: 'date', - }, - { - name: 'collector_certificate', - type: 'short', - }, - { - name: 'exporter_certificate', - type: 'short', - }, - { - name: 'data_records_reliability', - type: 'boolean', - }, - { - name: 'observation_point_type', - type: 'short', - }, - { - name: 'new_connection_delta_count', - type: 'long', - }, - { - name: 'connection_sum_duration_seconds', - type: 'long', - }, - { - name: 'connection_transaction_id', - type: 'long', - }, - { - name: 'post_nat_source_ipv6_address', - type: 'ip', - }, - { - name: 'post_nat_destination_ipv6_address', - type: 'ip', - }, - { - name: 'nat_pool_id', - type: 'long', - }, - { - name: 'nat_pool_name', - type: 'keyword', - }, - { - name: 'anonymization_flags', - type: 'integer', - }, - { - name: 'anonymization_technique', - type: 'integer', - }, - { - name: 'information_element_index', - type: 'integer', - }, - { - name: 'p2p_technology', - type: 'keyword', - }, - { - name: 'tunnel_technology', - type: 'keyword', - }, - { - name: 'encrypted_technology', - type: 'keyword', - }, - { - name: 'bgp_validity_state', - type: 'short', - }, - { - name: 'ip_sec_spi', - type: 'long', - }, - { - name: 'gre_key', - type: 'long', - }, - { - name: 'nat_type', - type: 'short', - }, - { - name: 'initiator_packets', - type: 'long', - }, - { - name: 'responder_packets', - type: 'long', - }, - { - name: 'observation_domain_name', - type: 'keyword', - }, - { - name: 'selection_sequence_id', - type: 'long', - }, - { - name: 'selector_id', - type: 'long', - }, - { - name: 'information_element_id', - type: 'integer', - }, - { - name: 'selector_algorithm', - type: 'integer', - }, - { - name: 'sampling_packet_interval', - type: 'long', - }, - { - name: 'sampling_packet_space', - type: 'long', - }, - { - name: 'sampling_time_interval', - type: 'long', - }, - { - name: 'sampling_time_space', - type: 'long', - }, - { - name: 'sampling_size', - type: 'long', - }, - { - name: 'sampling_population', - type: 'long', - }, - { - name: 'sampling_probability', - type: 'double', - }, - { - name: 'data_link_frame_size', - type: 'integer', - }, - { - name: 'ip_header_packet_section', - type: 'short', - }, - { - name: 'ip_payload_packet_section', - type: 'short', - }, - { - name: 'data_link_frame_section', - type: 'short', - }, - { - name: 'mpls_label_stack_section', - type: 'short', - }, - { - name: 'mpls_payload_packet_section', - type: 'short', - }, - { - name: 'selector_id_total_pkts_observed', - type: 'long', - }, - { - name: 'selector_id_total_pkts_selected', - type: 'long', - }, - { - name: 'absolute_error', - type: 'double', - }, - { - name: 'relative_error', - type: 'double', - }, - { - name: 'observation_time_seconds', - type: 'date', - }, - { - name: 'observation_time_milliseconds', - type: 'date', - }, - { - name: 'observation_time_microseconds', - type: 'date', - }, - { - name: 'observation_time_nanoseconds', - type: 'date', - }, - { - name: 'digest_hash_value', - type: 'long', - }, - { - name: 'hash_ip_payload_offset', - type: 'long', - }, - { - name: 'hash_ip_payload_size', - type: 'long', - }, - { - name: 'hash_output_range_min', - type: 'long', - }, - { - name: 'hash_output_range_max', - type: 'long', - }, - { - name: 'hash_selected_range_min', - type: 'long', - }, - { - name: 'hash_selected_range_max', - type: 'long', - }, - { - name: 'hash_digest_output', - type: 'boolean', - }, - { - name: 'hash_initialiser_value', - type: 'long', - }, - { - name: 'selector_name', - type: 'keyword', - }, - { - name: 'upper_ci_limit', - type: 'double', - }, - { - name: 'lower_ci_limit', - type: 'double', - }, - { - name: 'confidence_level', - type: 'double', - }, - { - name: 'information_element_data_type', - type: 'short', - }, - { - name: 'information_element_description', - type: 'keyword', - }, - { - name: 'information_element_name', - type: 'keyword', - }, - { - name: 'information_element_range_begin', - type: 'long', - }, - { - name: 'information_element_range_end', - type: 'long', - }, - { - name: 'information_element_semantics', - type: 'short', - }, - { - name: 'information_element_units', - type: 'integer', - }, - { - name: 'private_enterprise_number', - type: 'long', - }, - { - name: 'virtual_station_interface_id', - type: 'short', - }, - { - name: 'virtual_station_interface_name', - type: 'keyword', - }, - { - name: 'virtual_station_uuid', - type: 'short', - }, - { - name: 'virtual_station_name', - type: 'keyword', - }, - { - name: 'layer2_segment_id', - type: 'long', - }, - { - name: 'layer2_octet_delta_count', - type: 'long', - }, - { - name: 'layer2_octet_total_count', - type: 'long', - }, - { - name: 'ingress_unicast_packet_total_count', - type: 'long', - }, - { - name: 'ingress_multicast_packet_total_count', - type: 'long', - }, - { - name: 'ingress_broadcast_packet_total_count', - type: 'long', - }, - { - name: 'egress_unicast_packet_total_count', - type: 'long', - }, - { - name: 'egress_broadcast_packet_total_count', - type: 'long', - }, - { - name: 'monitoring_interval_start_milli_seconds', - type: 'date', - }, - { - name: 'monitoring_interval_end_milli_seconds', - type: 'date', - }, - { - name: 'port_range_start', - type: 'integer', - }, - { - name: 'port_range_end', - type: 'integer', - }, - { - name: 'port_range_step_size', - type: 'integer', - }, - { - name: 'port_range_num_ports', - type: 'integer', - }, - { - name: 'sta_mac_address', - type: 'keyword', - }, - { - name: 'sta_ipv4_address', - type: 'ip', - }, - { - name: 'wtp_mac_address', - type: 'keyword', - }, - { - name: 'ingress_interface_type', - type: 'long', - }, - { - name: 'egress_interface_type', - type: 'long', - }, - { - name: 'rtp_sequence_number', - type: 'integer', - }, - { - name: 'user_name', - type: 'keyword', - }, - { - name: 'application_category_name', - type: 'keyword', - }, - { - name: 'application_sub_category_name', - type: 'keyword', - }, - { - name: 'application_group_name', - type: 'keyword', - }, - { - name: 'original_flows_present', - type: 'long', - }, - { - name: 'original_flows_initiated', - type: 'long', - }, - { - name: 'original_flows_completed', - type: 'long', - }, - { - name: 'distinct_count_of_source_ip_address', - type: 'long', - }, - { - name: 'distinct_count_of_destination_ip_address', - type: 'long', - }, - { - name: 'distinct_count_of_source_ipv4_address', - type: 'long', - }, - { - name: 'distinct_count_of_destination_ipv4_address', - type: 'long', - }, - { - name: 'distinct_count_of_source_ipv6_address', - type: 'long', - }, - { - name: 'distinct_count_of_destination_ipv6_address', - type: 'long', - }, - { - name: 'value_distribution_method', - type: 'short', - }, - { - name: 'rfc3550_jitter_milliseconds', - type: 'long', - }, - { - name: 'rfc3550_jitter_microseconds', - type: 'long', - }, - { - name: 'rfc3550_jitter_nanoseconds', - type: 'long', - }, - { - name: 'dot1q_dei', - type: 'boolean', - }, - { - name: 'dot1q_customer_dei', - type: 'boolean', - }, - { - name: 'flow_selector_algorithm', - type: 'integer', - }, - { - name: 'flow_selected_octet_delta_count', - type: 'long', - }, - { - name: 'flow_selected_packet_delta_count', - type: 'long', - }, - { - name: 'flow_selected_flow_delta_count', - type: 'long', - }, - { - name: 'selector_id_total_flows_observed', - type: 'long', - }, - { - name: 'selector_id_total_flows_selected', - type: 'long', - }, - { - name: 'sampling_flow_interval', - type: 'long', - }, - { - name: 'sampling_flow_spacing', - type: 'long', - }, - { - name: 'flow_sampling_time_interval', - type: 'long', - }, - { - name: 'flow_sampling_time_spacing', - type: 'long', - }, - { - name: 'hash_flow_domain', - type: 'integer', - }, - { - name: 'transport_octet_delta_count', - type: 'long', - }, - { - name: 'transport_packet_delta_count', - type: 'long', - }, - { - name: 'original_exporter_ipv4_address', - type: 'ip', - }, - { - name: 'original_exporter_ipv6_address', - type: 'ip', - }, - { - name: 'original_observation_domain_id', - type: 'long', - }, - { - name: 'intermediate_process_id', - type: 'long', - }, - { - name: 'ignored_data_record_total_count', - type: 'long', - }, - { - name: 'data_link_frame_type', - type: 'integer', - }, - { - name: 'section_offset', - type: 'integer', - }, - { - name: 'section_exported_octets', - type: 'integer', - }, - { - name: 'dot1q_service_instance_tag', - type: 'short', - }, - { - name: 'dot1q_service_instance_id', - type: 'long', - }, - { - name: 'dot1q_service_instance_priority', - type: 'short', - }, - { - name: 'dot1q_customer_source_mac_address', - type: 'keyword', - }, - { - name: 'dot1q_customer_destination_mac_address', - type: 'keyword', - }, - { - name: 'post_layer2_octet_delta_count', - type: 'long', - }, - { - name: 'post_mcast_layer2_octet_delta_count', - type: 'long', - }, - { - name: 'post_layer2_octet_total_count', - type: 'long', - }, - { - name: 'post_mcast_layer2_octet_total_count', - type: 'long', - }, - { - name: 'minimum_layer2_total_length', - type: 'long', - }, - { - name: 'maximum_layer2_total_length', - type: 'long', - }, - { - name: 'dropped_layer2_octet_delta_count', - type: 'long', - }, - { - name: 'dropped_layer2_octet_total_count', - type: 'long', - }, - { - name: 'ignored_layer2_octet_total_count', - type: 'long', - }, - { - name: 'not_sent_layer2_octet_total_count', - type: 'long', - }, - { - name: 'layer2_octet_delta_sum_of_squares', - type: 'long', - }, - { - name: 'layer2_octet_total_sum_of_squares', - type: 'long', - }, - { - name: 'layer2_frame_delta_count', - type: 'long', - }, - { - name: 'layer2_frame_total_count', - type: 'long', - }, - { - name: 'pseudo_wire_destination_ipv4_address', - type: 'ip', - }, - { - name: 'ignored_layer2_frame_total_count', - type: 'long', - }, - { - name: 'mib_object_value_integer', - type: 'integer', - }, - { - name: 'mib_object_value_octet_string', - type: 'short', - }, - { - name: 'mib_object_value_oid', - type: 'short', - }, - { - name: 'mib_object_value_bits', - type: 'short', - }, - { - name: 'mib_object_value_ip_address', - type: 'ip', - }, - { - name: 'mib_object_value_counter', - type: 'long', - }, - { - name: 'mib_object_value_gauge', - type: 'long', - }, - { - name: 'mib_object_value_time_ticks', - type: 'long', - }, - { - name: 'mib_object_value_unsigned', - type: 'long', - }, - { - name: 'mib_object_identifier', - type: 'short', - }, - { - name: 'mib_sub_identifier', - type: 'long', - }, - { - name: 'mib_index_indicator', - type: 'long', - }, - { - name: 'mib_capture_time_semantics', - type: 'short', - }, - { - name: 'mib_context_engine_id', - type: 'short', - }, - { - name: 'mib_context_name', - type: 'keyword', - }, - { - name: 'mib_object_name', - type: 'keyword', - }, - { - name: 'mib_object_description', - type: 'keyword', - }, - { - name: 'mib_object_syntax', - type: 'keyword', - }, - { - name: 'mib_module_name', - type: 'keyword', - }, - { - name: 'mobile_imsi', - type: 'keyword', - }, - { - name: 'mobile_msisdn', - type: 'keyword', - }, - { - name: 'http_status_code', - type: 'integer', - }, - { - name: 'source_transport_ports_limit', - type: 'integer', - }, - { - name: 'http_request_method', - type: 'keyword', - }, - { - name: 'http_request_host', - type: 'keyword', - }, - { - name: 'http_request_target', - type: 'keyword', - }, - { - name: 'http_message_version', - type: 'keyword', - }, - { - name: 'nat_instance_id', - type: 'long', - }, - { - name: 'internal_address_realm', - type: 'short', - }, - { - name: 'external_address_realm', - type: 'short', - }, - { - name: 'nat_quota_exceeded_event', - type: 'long', - }, - { - name: 'nat_threshold_event', - type: 'long', - }, - { - name: 'http_user_agent', - type: 'keyword', - }, - { - name: 'http_content_type', - type: 'keyword', - }, - { - name: 'http_reason_phrase', - type: 'keyword', - }, - { - name: 'max_session_entries', - type: 'long', - }, - { - name: 'max_bib_entries', - type: 'long', - }, - { - name: 'max_entries_per_user', - type: 'long', - }, - { - name: 'max_subscribers', - type: 'long', - }, - { - name: 'max_fragments_pending_reassembly', - type: 'long', - }, - { - name: 'address_pool_high_threshold', - type: 'long', - }, - { - name: 'address_pool_low_threshold', - type: 'long', - }, - { - name: 'address_port_mapping_high_threshold', - type: 'long', - }, - { - name: 'address_port_mapping_low_threshold', - type: 'long', - }, - { - name: 'address_port_mapping_per_user_high_threshold', - type: 'long', - }, - { - name: 'global_address_mapping_high_threshold', - type: 'long', - }, - { - name: 'vpn_identifier', - type: 'short', - }, - ], - }, - ], - }, - { - key: 's3', - title: 's3', - description: 'S3 fields from s3 input.\n', - release: 'beta', - fields: [ - { - name: 'bucket_name', - type: 'keyword', - description: 'Name of the S3 bucket that this log retrieved from.\n', - }, - { - name: 'object_key', - type: 'keyword', - description: 'Name of the S3 object that this log retrieved from.\n', - }, - ], - }, - { - key: 'cef', - title: 'Decode CEF processor fields', - description: 'Common Event Format (CEF) data.\n', - fields: [ - { - name: 'cef', - type: 'group', - description: - 'By default the `decode_cef` processor writes all data from the CEF message to this `cef` object. It contains the CEF header fields and the extension data.\n', - fields: [ - { - name: 'version', - type: 'keyword', - description: 'Version of the CEF specification used by the message.\n', - }, - { - name: 'device.vendor', - type: 'keyword', - description: 'Vendor of the device that produced the message.\n', - }, - { - name: 'device.product', - type: 'keyword', - description: 'Product of the device that produced the message.\n', - }, - { - name: 'device.version', - type: 'keyword', - description: 'Version of the product that produced the message.\n', - }, - { - name: 'device.event_class_id', - type: 'keyword', - description: 'Unique identifier of the event type.\n', - }, - { - name: 'severity', - type: 'keyword', - example: 'Very-High', - description: - 'Importance of the event. The valid string values are Unknown, Low, Medium, High, and Very-High. The valid integer values are 0-3=Low, 4-6=Medium, 7- 8=High, and 9-10=Very-High.\n', - }, - { - name: 'name', - type: 'keyword', - description: 'Short description of the event.\n', - }, - { - name: 'extensions', - type: 'group', - description: 'Collection of key-value pairs carried in the CEF extension field.\n', - default_field: false, - fields: [ - { - name: 'agentAddress', - type: 'ip', - description: 'The IP address of the ArcSight connector that processed the event.', - }, - { - name: 'agentDnsDomain', - type: 'keyword', - description: - 'The DNS domain name of the ArcSight connector that processed the event.', - }, - { - name: 'agentHostName', - type: 'keyword', - description: 'The hostname of the ArcSight connector that processed the event.', - }, - { - name: 'agentId', - type: 'keyword', - description: 'The agent ID of the ArcSight connector that processed the event.', - }, - { - name: 'agentMacAddress', - type: 'keyword', - description: 'The MAC address of the ArcSight connector that processed the event.', - }, - { - name: 'agentNtDomain', - type: 'keyword', - description: '', - }, - { - name: 'agentReceiptTime', - type: 'date', - description: - 'The time at which information about the event was received by the ArcSight connector.', - }, - { - name: 'agentTimeZone', - type: 'keyword', - description: - 'The agent time zone of the ArcSight connector that processed the event.', - }, - { - name: 'agentTranslatedAddress', - type: 'ip', - description: '', - }, - { - name: 'agentTranslatedZoneExternalID', - type: 'keyword', - description: '', - }, - { - name: 'agentTranslatedZoneURI', - type: 'keyword', - description: '', - }, - { - name: 'agentType', - type: 'keyword', - description: 'The agent type of the ArcSight connector that processed the event', - }, - { - name: 'agentVersion', - type: 'keyword', - description: 'The version of the ArcSight connector that processed the event.', - }, - { - name: 'agentZoneExternalID', - type: 'keyword', - description: '', - }, - { - name: 'agentZoneURI', - type: 'keyword', - description: '', - }, - { - name: 'applicationProtocol', - type: 'keyword', - description: - 'Application level protocol, example values are HTTP, HTTPS, SSHv2, Telnet, POP, IMPA, IMAPS, and so on.', - }, - { - name: 'baseEventCount', - type: 'long', - description: - 'A count associated with this event. How many times was this same event observed? Count can be omitted if it is 1.', - }, - { - name: 'bytesIn', - type: 'long', - description: - 'Number of bytes transferred inbound, relative to the source to destination relationship, meaning that data was flowing from source to destination.', - }, - { - name: 'bytesOut', - type: 'long', - description: - 'Number of bytes transferred outbound relative to the source to destination relationship. For example, the byte number of data flowing from the destination to the source.', - }, - { - name: 'customerExternalID', - type: 'keyword', - description: '', - }, - { - name: 'customerURI', - type: 'keyword', - description: '', - }, - { - name: 'destinationAddress', - type: 'ip', - description: - 'Identifies the destination address that the event refers to in an IP network. The format is an IPv4 address.', - }, - { - name: 'destinationDnsDomain', - type: 'keyword', - description: - 'The DNS domain part of the complete fully qualified domain name (FQDN).', - }, - { - name: 'destinationGeoLatitude', - type: 'double', - description: - "The latitudinal value from which the destination's IP address belongs.", - }, - { - name: 'destinationGeoLongitude', - type: 'double', - description: - "The longitudinal value from which the destination's IP address belongs.", - }, - { - name: 'destinationHostName', - type: 'keyword', - description: - 'Identifies the destination that an event refers to in an IP network. The format should be a fully qualified domain name (FQDN) associated with the destination node, when a node is available.', - }, - { - name: 'destinationMacAddress', - type: 'keyword', - description: 'Six colon-seperated hexadecimal numbers.', - }, - { - name: 'destinationNtDomain', - type: 'keyword', - description: 'The Windows domain name of the destination address.', - }, - { - name: 'destinationPort', - type: 'long', - description: 'The valid port numbers are between 0 and 65535.', - }, - { - name: 'destinationProcessId', - type: 'long', - description: - 'Provides the ID of the destination process associated with the event. For example, if an event contains process ID 105, "105" is the process ID.', - }, - { - name: 'destinationProcessName', - type: 'keyword', - description: "The name of the event's destination process.", - }, - { - name: 'destinationServiceName', - type: 'keyword', - description: 'The service targeted by this event.', - }, - { - name: 'destinationTranslatedAddress', - type: 'ip', - description: - 'Identifies the translated destination that the event refers to in an IP network.', - }, - { - name: 'destinationTranslatedPort', - type: 'long', - description: - 'Port after it was translated; for example, a firewall. Valid port numbers are 0 to 65535.', - }, - { - name: 'destinationTranslatedZoneExternalID', - type: 'keyword', - description: '', - }, - { - name: 'destinationTranslatedZoneURI', - type: 'keyword', - description: - 'The URI for the Translated Zone that the destination asset has been assigned to in ArcSight.', - }, - { - name: 'destinationUserId', - type: 'keyword', - description: - 'Identifies the destination user by ID. For example, in UNIX, the root user is generally associated with user ID 0.', - }, - { - name: 'destinationUserName', - type: 'keyword', - description: - "Identifies the destination user by name. This is the user associated with the event's destination. Email addresses are often mapped into the UserName fields. The recipient is a candidate to put into this field.", - }, - { - name: 'destinationUserPrivileges', - type: 'keyword', - description: - 'The typical values are "Administrator", "User", and "Guest". This identifies the destination user\'s privileges. In UNIX, for example, activity executed on the root user would be identified with destinationUser Privileges of "Administrator".', - }, - { - name: 'destinationZoneExternalID', - type: 'keyword', - description: '', - }, - { - name: 'destinationZoneURI', - type: 'keyword', - description: - 'The URI for the Zone that the destination asset has been assigned to in ArcSight.', - }, - { - name: 'deviceAction', - type: 'keyword', - description: 'Action taken by the device.', - }, - { - name: 'deviceAddress', - type: 'ip', - description: - 'Identifies the device address that an event refers to in an IP network.', - }, - { - name: 'deviceCustomFloatingPoint1Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceCustomFloatingPoint3Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceCustomFloatingPoint4Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceCustomDate1', - type: 'date', - description: - 'One of two timestamp fields available to map fields that do not apply to any other in this dictionary.', - }, - { - name: 'deviceCustomDate1Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceCustomDate2', - type: 'date', - description: - 'One of two timestamp fields available to map fields that do not apply to any other in this dictionary.', - }, - { - name: 'deviceCustomDate2Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceCustomFloatingPoint1', - type: 'double', - description: - 'One of four floating point fields available to map fields that do not apply to any other in this dictionary.', - }, - { - name: 'deviceCustomFloatingPoint2', - type: 'double', - description: - 'One of four floating point fields available to map fields that do not apply to any other in this dictionary.', - }, - { - name: 'deviceCustomFloatingPoint2Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceCustomFloatingPoint3', - type: 'double', - description: - 'One of four floating point fields available to map fields that do not apply to any other in this dictionary.', - }, - { - name: 'deviceCustomFloatingPoint4', - type: 'double', - description: - 'One of four floating point fields available to map fields that do not apply to any other in this dictionary.', - }, - { - name: 'deviceCustomIPv6Address1', - type: 'ip', - description: - 'One of four IPv6 address fields available to map fields that do not apply to any other in this dictionary.', - }, - { - name: 'deviceCustomIPv6Address1Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceCustomIPv6Address2', - type: 'ip', - description: - 'One of four IPv6 address fields available to map fields that do not apply to any other in this dictionary.', - }, - { - name: 'deviceCustomIPv6Address2Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceCustomIPv6Address3', - type: 'ip', - description: - 'One of four IPv6 address fields available to map fields that do not apply to any other in this dictionary.', - }, - { - name: 'deviceCustomIPv6Address3Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceCustomIPv6Address4', - type: 'ip', - description: - 'One of four IPv6 address fields available to map fields that do not apply to any other in this dictionary.', - }, - { - name: 'deviceCustomIPv6Address4Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceCustomNumber1', - type: 'long', - description: - 'One of three number fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', - }, - { - name: 'deviceCustomNumber1Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceCustomNumber2', - type: 'long', - description: - 'One of three number fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', - }, - { - name: 'deviceCustomNumber2Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceCustomNumber3', - type: 'long', - description: - 'One of three number fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', - }, - { - name: 'deviceCustomNumber3Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceCustomString1', - type: 'keyword', - description: - 'One of six strings available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', - }, - { - name: 'deviceCustomString1Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceCustomString2', - type: 'keyword', - description: - 'One of six strings available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', - }, - { - name: 'deviceCustomString2Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceCustomString3', - type: 'keyword', - description: - 'One of six strings available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', - }, - { - name: 'deviceCustomString3Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceCustomString4', - type: 'keyword', - description: - 'One of six strings available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', - }, - { - name: 'deviceCustomString4Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceCustomString5', - type: 'keyword', - description: - 'One of six strings available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', - }, - { - name: 'deviceCustomString5Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceCustomString6', - type: 'keyword', - description: - 'One of six strings available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', - }, - { - name: 'deviceCustomString6Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceDirection', - type: 'long', - description: - 'Any information about what direction the observed communication has taken. The following values are supported - "0" for inbound or "1" for outbound.', - }, - { - name: 'deviceDnsDomain', - type: 'keyword', - description: - 'The DNS domain part of the complete fully qualified domain name (FQDN).', - }, - { - name: 'deviceEventCategory', - type: 'keyword', - description: - 'Represents the category assigned by the originating device. Devices often use their own categorization schema to classify event. Example "/Monitor/Disk/Read".', - }, - { - name: 'deviceExternalId', - type: 'keyword', - description: 'A name that uniquely identifies the device generating this event.', - }, - { - name: 'deviceFacility', - type: 'keyword', - description: - 'The facility generating this event. For example, Syslog has an explicit facility associated with every event.', - }, - { - name: 'deviceFlexNumber1', - type: 'long', - description: - 'One of two alternative number fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', - }, - { - name: 'deviceFlexNumber1Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceFlexNumber2', - type: 'long', - description: - 'One of two alternative number fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', - }, - { - name: 'deviceFlexNumber2Label', - type: 'keyword', - description: - 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', - }, - { - name: 'deviceHostName', - type: 'keyword', - description: - 'The format should be a fully qualified domain name (FQDN) associated with the device node, when a node is available.', - }, - { - name: 'deviceInboundInterface', - type: 'keyword', - description: 'Interface on which the packet or data entered the device.', - }, - { - name: 'deviceMacAddress', - type: 'keyword', - description: 'Six colon-separated hexadecimal numbers.', - }, - { - name: 'deviceNtDomain', - type: 'keyword', - description: 'The Windows domain name of the device address.', - }, - { - name: 'deviceOutboundInterface', - type: 'keyword', - description: 'Interface on which the packet or data left the device.', - }, - { - name: 'devicePayloadId', - type: 'keyword', - description: 'Unique identifier for the payload associated with the event.', - }, - { - name: 'deviceProcessId', - type: 'long', - description: 'Provides the ID of the process on the device generating the event.', - }, - { - name: 'deviceProcessName', - type: 'keyword', - description: - 'Process name associated with the event. An example might be the process generating the syslog entry in UNIX.', - }, - { - name: 'deviceReceiptTime', - type: 'date', - description: - 'The time at which the event related to the activity was received. The format is MMM dd yyyy HH:mm:ss or milliseconds since epoch (Jan 1st 1970)', - }, - { - name: 'deviceTimeZone', - type: 'keyword', - description: 'The timezone for the device generating the event.', - }, - { - name: 'deviceTranslatedAddress', - type: 'ip', - description: - 'Identifies the translated device address that the event refers to in an IP network.', - }, - { - name: 'deviceTranslatedZoneExternalID', - type: 'keyword', - description: '', - }, - { - name: 'deviceTranslatedZoneURI', - type: 'keyword', - description: - 'The URI for the Translated Zone that the device asset has been assigned to in ArcSight.', - }, - { - name: 'deviceZoneExternalID', - type: 'keyword', - description: '', - }, - { - name: 'deviceZoneURI', - type: 'keyword', - description: - 'Thee URI for the Zone that the device asset has been assigned to in ArcSight.', - }, - { - name: 'endTime', - type: 'date', - description: - 'The time at which the activity related to the event ended. The format is MMM dd yyyy HH:mm:ss or milliseconds since epoch (Jan 1st1970). An example would be reporting the end of a session.', - }, - { - name: 'eventId', - type: 'long', - description: 'This is a unique ID that ArcSight assigns to each event.', - }, - { - name: 'eventOutcome', - type: 'keyword', - description: "Displays the outcome, usually as 'success' or 'failure'.", - }, - { - name: 'externalId', - type: 'keyword', - description: - 'The ID used by an originating device. They are usually increasing numbers, associated with events.', - }, - { - name: 'fileCreateTime', - type: 'date', - description: 'Time when the file was created.', - }, - { - name: 'fileHash', - type: 'keyword', - description: 'Hash of a file.', - }, - { - name: 'fileId', - type: 'keyword', - description: 'An ID associated with a file could be the inode.', - }, - { - name: 'fileModificationTime', - type: 'date', - description: 'Time when the file was last modified.', - }, - { - name: 'filename', - type: 'keyword', - description: 'Name of the file only (without its path).', - }, - { - name: 'filePath', - type: 'keyword', - description: 'Full path to the file, including file name itself.', - }, - { - name: 'filePermission', - type: 'keyword', - description: 'Permissions of the file.', - }, - { - name: 'fileSize', - type: 'long', - description: 'Size of the file.', - }, - { - name: 'fileType', - type: 'keyword', - description: 'Type of file (pipe, socket, etc.)', - }, - { - name: 'flexDate1', - type: 'date', - description: - 'A timestamp field available to map a timestamp that does not apply to any other defined timestamp field in this dictionary. Use all flex fields sparingly and seek a more specific, dictionary supplied field when possible. These fields are typically reserved for customer use and should not be set by vendors unless necessary.', - }, - { - name: 'flexDate1Label', - type: 'keyword', - description: - 'The label field is a string and describes the purpose of the flex field.', - }, - { - name: 'flexString1', - type: 'keyword', - description: - 'One of four floating point fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible. These fields are typically reserved for customer use and should not be set by vendors unless necessary.', - }, - { - name: 'flexString2', - type: 'keyword', - description: - 'One of four floating point fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible. These fields are typically reserved for customer use and should not be set by vendors unless necessary.', - }, - { - name: 'flexString1Label', - type: 'keyword', - description: - 'The label field is a string and describes the purpose of the flex field.', - }, - { - name: 'flexString2Label', - type: 'keyword', - description: - 'The label field is a string and describes the purpose of the flex field.', - }, - { - name: 'message', - type: 'keyword', - description: - 'An arbitrary message giving more details about the event. Multi-line entries can be produced by using \\n as the new line separator.', - }, - { - name: 'oldFileCreateTime', - type: 'date', - description: 'Time when old file was created.', - }, - { - name: 'oldFileHash', - type: 'keyword', - description: 'Hash of the old file.', - }, - { - name: 'oldFileId', - type: 'keyword', - description: 'An ID associated with the old file could be the inode.', - }, - { - name: 'oldFileModificationTime', - type: 'date', - description: 'Time when old file was last modified.', - }, - { - name: 'oldFileName', - type: 'keyword', - description: 'Name of the old file.', - }, - { - name: 'oldFilePath', - type: 'keyword', - description: 'Full path to the old file, including the file name itself.', - }, - { - name: 'oldFilePermission', - type: 'keyword', - description: 'Permissions of the old file.', - }, - { - name: 'oldFileSize', - type: 'long', - description: 'Size of the old file.', - }, - { - name: 'oldFileType', - type: 'keyword', - description: 'Type of the old file (pipe, socket, etc.)', - }, - { - name: 'rawEvent', - type: 'keyword', - description: '', - }, - { - name: 'Reason', - type: 'keyword', - description: - 'The reason an audit event was generated. For example "bad password" or "unknown user". This could also be an error or return code. Example "0x1234".', - }, - { - name: 'requestClientApplication', - type: 'keyword', - description: 'The User-Agent associated with the request.', - }, - { - name: 'requestContext', - type: 'keyword', - description: - 'Description of the content from which the request originated (for example, HTTP Referrer)', - }, - { - name: 'requestCookies', - type: 'keyword', - description: 'Cookies associated with the request.', - }, - { - name: 'requestMethod', - type: 'keyword', - description: 'The HTTP method used to access a URL.', - }, - { - name: 'requestUrl', - type: 'keyword', - description: - 'In the case of an HTTP request, this field contains the URL accessed. The URL should contain the protocol as well.', - }, - { - name: 'sourceAddress', - type: 'ip', - description: 'Identifies the source that an event refers to in an IP network.', - }, - { - name: 'sourceDnsDomain', - type: 'keyword', - description: - 'The DNS domain part of the complete fully qualified domain name (FQDN).', - }, - { - name: 'sourceGeoLatitude', - type: 'double', - description: '', - }, - { - name: 'sourceGeoLongitude', - type: 'double', - description: '', - }, - { - name: 'sourceHostName', - type: 'keyword', - description: - "Identifies the source that an event refers to in an IP network. The format should be a fully qualified domain name (FQDN) associated with the source node, when a mode is available. Examples: 'host' or 'host.domain.com'.\n", - }, - { - name: 'sourceMacAddress', - type: 'keyword', - example: '00:0d:60:af:1b:61', - description: 'Six colon-separated hexadecimal numbers.', - }, - { - name: 'sourceNtDomain', - type: 'keyword', - description: 'The Windows domain name for the source address.', - }, - { - name: 'sourcePort', - type: 'long', - description: 'The valid port numbers are 0 to 65535.', - }, - { - name: 'sourceProcessId', - type: 'long', - description: 'The ID of the source process associated with the event.', - }, - { - name: 'sourceProcessName', - type: 'keyword', - description: "The name of the event's source process.", - }, - { - name: 'sourceServiceName', - type: 'keyword', - description: 'The service that is responsible for generating this event.', - }, - { - name: 'sourceTranslatedAddress', - type: 'ip', - description: - 'Identifies the translated source that the event refers to in an IP network.', - }, - { - name: 'sourceTranslatedPort', - type: 'long', - description: - 'A port number after being translated by, for example, a firewall. Valid port numbers are 0 to 65535.', - }, - { - name: 'sourceTranslatedZoneExternalID', - type: 'keyword', - description: '', - }, - { - name: 'sourceTranslatedZoneURI', - type: 'keyword', - description: - 'The URI for the Translated Zone that the destination asset has been assigned to in ArcSight.', - }, - { - name: 'sourceUserId', - type: 'keyword', - description: - 'Identifies the source user by ID. This is the user associated with the source of the event. For example, in UNIX, the root user is generally associated with user ID 0.', - }, - { - name: 'sourceUserName', - type: 'keyword', - description: - 'Identifies the source user by name. Email addresses are also mapped into the UserName fields. The sender is a candidate to put into this field.', - }, - { - name: 'sourceUserPrivileges', - type: 'keyword', - description: - 'The typical values are "Administrator", "User", and "Guest". It identifies the source user\'s privileges. In UNIX, for example, activity executed by the root user would be identified with "Administrator".', - }, - { - name: 'sourceZoneExternalID', - type: 'keyword', - description: '', - }, - { - name: 'sourceZoneURI', - type: 'keyword', - description: - 'The URI for the Zone that the source asset has been assigned to in ArcSight.', - }, - { - name: 'startTime', - type: 'date', - description: - 'The time when the activity the event referred to started. The format is MMM dd yyyy HH:mm:ss or milliseconds since epoch (Jan 1st 1970)', - }, - { - name: 'transportProtocol', - type: 'keyword', - description: - 'Identifies the Layer-4 protocol used. The possible values are protocols such as TCP or UDP.', - }, - { - name: 'type', - type: 'long', - description: - '0 means base event, 1 means aggregated, 2 means correlation, and 3 means action. This field can be omitted for base events (type 0).', - }, - { - name: 'categoryDeviceType', - type: 'keyword', - description: 'Device type. Examples - Proxy, IDS, Web Server', - }, - { - name: 'categoryObject', - type: 'keyword', - description: - 'Object that the event is about. For example it can be an operating sytem, database, file, etc.', - }, - { - name: 'categoryBehavior', - type: 'keyword', - description: - "Action or a behavior associated with an event. It's what is being done to the object.", - }, - { - name: 'categoryTechnique', - type: 'keyword', - description: 'Technique being used (e.g. /DoS).', - }, - { - name: 'categoryDeviceGroup', - type: 'keyword', - description: 'General device group like Firewall.', - }, - { - name: 'categorySignificance', - type: 'keyword', - description: 'Characterization of the importance of the event.', - }, - { - name: 'categoryOutcome', - type: 'keyword', - description: 'Outcome of the event (e.g. sucess, failure, or attempt).', - }, - { - name: 'managerReceiptTime', - type: 'date', - description: 'When the Arcsight ESM received the event.', - }, - ], - }, - ], - }, - { - name: 'source.service.name', - type: 'keyword', - description: 'Service that is the source of the event.', - }, - { - name: 'destination.service.name', - type: 'keyword', - description: 'Service that is the target of the event.', - }, - ], - }, -]; diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/index.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/index.ts deleted file mode 100644 index bd7e7d4eec83b..0000000000000 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { auditbeatSchema } from './auditbeat'; -export { filebeatSchema } from './filebeat'; -export { packetbeatSchema } from './packetbeat'; -export { winlogbeatSchema } from './winlogbeat'; -export { ecsSchema } from './ecs'; - -export const extraSchemaField = { - _id: { - description: 'Each document has an _id that uniquely identifies it', - example: 'Y-6TfmcB0WOhS6qyMv3s', - footnote: '', - group: 1, - level: 'core', - name: '_id', - required: true, - type: 'keyword', - }, - _index: { - description: - 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', - example: 'auditbeat-8.0.0-2019.02.19-000001', - footnote: '', - group: 1, - level: 'core', - name: '_index', - required: true, - type: 'keyword', - }, -}; - -export const baseCategoryFields = ['@timestamp', 'labels', 'message', 'tags']; diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/packetbeat.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/packetbeat.ts deleted file mode 100644 index 0be2e48fe4668..0000000000000 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/packetbeat.ts +++ /dev/null @@ -1,8556 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -/** - * An instance of the unmodified schema exported from auditbeat-8.0.0-SNAPSHOT-darwin-x86_64.tar.gz - * - */ - -import { Schema } from '../type'; - -export const packetbeatSchema: Schema = [ - { - key: 'ecs', - title: 'ECS', - description: 'ECS Fields.', - fields: [ - { - name: '@timestamp', - level: 'core', - required: true, - type: 'date', - description: - 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', - example: '2016-05-23T08:05:34.853Z', - }, - { - name: 'labels', - level: 'core', - type: 'object', - object_type: 'keyword', - description: - 'Custom key/value pairs.\n\nCan be used to add meta information to events. Should not contain nested objects.\nAll values are stored as keyword.\n\nExample: `docker` and `k8s` labels.', - example: '{"application": "foo-bar", "env": "production"}', - }, - { - name: 'message', - level: 'core', - type: 'text', - description: - 'For log events the message field contains the log message, optimized\nfor viewing in a log viewer.\n\nFor structured logs without an original message field, other fields can be concatenated\nto form a human-readable summary of the event.\n\nIf multiple messages exist, they can be combined into one message.', - example: 'Hello World', - }, - { - name: 'tags', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'List of keywords used to tag each event.', - example: '["production", "env2"]', - }, - { - name: 'agent', - title: 'Agent', - group: 2, - description: - 'The agent fields contain the data about the software entity, if\nany, that collects, detects, or observes events on a host, or takes measurements\non a host.\n\nExamples include Beats. Agents may also run on observers. ECS agent.* fields\nshall be populated with details of the agent running on the host or observer\nwhere the event happened or the measurement was taken.', - footnote: - 'Examples: In the case of Beats for logs, the agent.name is filebeat.\nFor APM, it is the agent running in the app/service. The agent information does\nnot change if data is sent through queuing systems like Kafka, Redis, or processing\nsystems such as Logstash or APM Server.', - type: 'group', - fields: [ - { - name: 'ephemeral_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Ephemeral identifier of this agent (if one exists).\n\nThis id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier of this agent (if one exists).\n\nExample: For Beats this would be beat.id.', - example: '8a4f500d', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', - example: 'foo', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', - example: 'filebeat', - }, - { - name: 'version', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Version of the agent.', - example: '6.0.0-rc2', - }, - ], - }, - { - name: 'as', - title: 'Autonomous System', - group: 2, - description: - 'An autonomous system (AS) is a collection of connected Internet Protocol\n(IP) routing prefixes under the control of one or more network operators on\nbehalf of a single administrative entity or domain that presents a common, clearly\ndefined routing policy to the internet.', - type: 'group', - fields: [ - { - name: 'number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - ], - }, - { - name: 'client', - title: 'Client', - group: 2, - description: - 'A client is defined as the initiator of a network connection for\nevents regarding sessions, connections, or bidirectional flow records.\n\nFor TCP events, the client is the initiator of the TCP connection that sends\nthe SYN packet(s). For other protocols, the client is generally the initiator\nor requestor in the network transaction. Some systems use the term "originator"\nto refer the client in TCP connections. The client fields describe details about\nthe system acting as the client in the network event. Client fields are usually\npopulated in conjunction with server fields. Client fields are generally not\npopulated for packet-level events.\n\nClient / server representations can add semantic context to an exchange, which\nis helpful to visualize the data in certain situations. If your context falls\nin that category, you should still ensure that source and destination are filled\nappropriately.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Some event client addresses are defined ambiguously. The event\nwill sometimes list an IP, a domain or a unix socket. You should always store\nthe raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'as.number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'as.organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the client to the server.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Client domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: - 'IP address of the client.\n\nCan be one or multiple IPv4 or IPv6 addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC address of the client.', - }, - { - name: 'nat.ip', - level: 'extended', - type: 'ip', - description: - 'Translated IP of source based NAT sessions (e.g. internal client\nto internet).\n\nTypically connections traversing load balancers, firewalls, or routers.', - }, - { - name: 'nat.port', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Translated port of source based NAT sessions (e.g. internal client\nto internet).\n\nTypically connections traversing load balancers, firewalls, or routers.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the client to the server.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the client.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered client domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'cloud', - title: 'Cloud', - group: 2, - description: 'Fields related to the cloud or infrastructure the events are coming\nfrom.', - footnote: - 'Examples: If Metricbeat is running on an EC2 host and fetches data\nfrom its host, the cloud info contains the data about this machine. If Metricbeat\nruns on a remote machine outside the cloud and fetches data from a service running\nin the cloud, the field contains cloud data from the machine the service is\nrunning on.', - type: 'group', - fields: [ - { - name: 'account.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The cloud account or organization id used to identify different\nentities in a multi-tenant environment.\n\nExamples: AWS account id, Google Cloud ORG Id, or other unique identifier.', - example: 666777888999, - }, - { - name: 'availability_zone', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Availability zone in which this host is running.', - example: 'us-east-1c', - }, - { - name: 'instance.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Instance ID of the host machine.', - example: 'i-1234567890abcdef0', - }, - { - name: 'instance.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Instance name of the host machine.', - }, - { - name: 'machine.type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Machine type of the host machine.', - example: 't2.medium', - }, - { - name: 'provider', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the cloud provider. Example values are aws, azure, gcp,\nor digitalocean.', - example: 'aws', - }, - { - name: 'region', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Region in which this host is running.', - example: 'us-east-1', - }, - ], - }, - { - name: 'code_signature', - title: 'Code Signature', - group: 2, - description: 'These fields contain information about binary code signatures.', - type: 'group', - fields: [ - { - name: 'exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - ], - }, - { - name: 'container', - title: 'Container', - group: 2, - description: - 'Container fields are used for meta information about the specific\ncontainer that is the source of information.\n\nThese fields help correlate data based containers from any runtime.', - type: 'group', - fields: [ - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique container id.', - }, - { - name: 'image.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the image the container was built on.', - }, - { - name: 'image.tag', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Container image tags.', - }, - { - name: 'labels', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: 'Image labels.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Container name.', - }, - { - name: 'runtime', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Runtime managing this container.', - example: 'docker', - }, - ], - }, - { - name: 'destination', - title: 'Destination', - group: 2, - description: - 'Destination fields describe details about the destination of a packet/event.\n\nDestination fields are usually populated in conjunction with source fields.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Some event destination addresses are defined ambiguously. The\nevent will sometimes list an IP, a domain or a unix socket. You should always\nstore the raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'as.number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'as.organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the destination to the source.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Destination domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: - 'IP address of the destination.\n\nCan be one or multiple IPv4 or IPv6 addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC address of the destination.', - }, - { - name: 'nat.ip', - level: 'extended', - type: 'ip', - description: - 'Translated ip of destination based NAT sessions (e.g. internet\nto private DMZ)\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'nat.port', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Port the source session is translated to by NAT Device.\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the destination to the source.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the destination.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered destination domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'dll', - title: 'DLL', - group: 2, - description: - 'These fields contain information about code libraries dynamically\nloaded into processes.\n\n\nMany operating systems refer to "shared code libraries" with different names,\nbut this field set refers to all of the following:\n\n* Dynamic-link library (`.dll`) commonly used on Windows\n\n* Shared Object (`.so`) commonly used on Unix-like operating systems\n\n* Dynamic library (`.dylib`) commonly used on macOS', - type: 'group', - fields: [ - { - name: 'code_signature.exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'code_signature.subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'code_signature.trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - { - name: 'hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - default_field: false, - }, - { - name: 'hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - default_field: false, - }, - { - name: 'hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - default_field: false, - }, - { - name: 'hash.sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - default_field: false, - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the library.\n\nThis generally maps to the name of the file on disk.', - example: 'kernel32.dll', - default_field: false, - }, - { - name: 'path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Full file path of the library.', - example: 'C:\\Windows\\System32\\kernel32.dll', - default_field: false, - }, - { - name: 'pe.company', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal company name of the file, provided at compile-time.', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'pe.description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal description of the file, provided at compile-time.', - example: 'Paint', - default_field: false, - }, - { - name: 'pe.file_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal version of the file, provided at compile-time.', - example: '6.3.9600.17415', - default_field: false, - }, - { - name: 'pe.original_file_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal name of the file, provided at compile-time.', - example: 'MSPAINT.EXE', - default_field: false, - }, - { - name: 'pe.product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal product name of the file, provided at compile-time.', - example: 'Microsoft® Windows® Operating System', - default_field: false, - }, - ], - }, - { - name: 'dns', - title: 'DNS', - group: 2, - description: - 'Fields describing DNS queries and answers.\n\nDNS events should either represent a single DNS query prior to getting answers\n(`dns.type:query`) or they should represent a full exchange and contain the\nquery details as well as all of the answers that were provided for this query\n(`dns.type:answer`).', - type: 'group', - fields: [ - { - name: 'answers', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'An array containing an object for each answer section returned\nby the server.\n\nThe main keys that should be present in these objects are defined by ECS.\nRecords that have more information may contain more keys than what ECS defines.\n\nNot all DNS data sources give all details about DNS answers. At minimum, answer\nobjects must contain the `data` key. If more information is available, map\nas much of it to ECS as possible, and add any additional fields to the answer\nobjects as custom fields.', - }, - { - name: 'answers.class', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The class of DNS data contained in this resource record.', - example: 'IN', - }, - { - name: 'answers.data', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The data describing the resource.\n\nThe meaning of this data depends on the type and class of the resource record.', - example: '10.10.10.10', - }, - { - name: 'answers.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The domain name to which this resource record pertains.\n\nIf a chain of CNAME is being resolved, each answer `name` should be the\none that corresponds with the answer `data`. It should not simply be the\noriginal `question.name` repeated.', - example: 'www.google.com', - }, - { - name: 'answers.ttl', - level: 'extended', - type: 'long', - description: - 'The time interval in seconds that this resource record may be cached\nbefore it should be discarded. Zero values mean that the data should not be\ncached.', - example: 180, - }, - { - name: 'answers.type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The type of data contained in this resource record.', - example: 'CNAME', - }, - { - name: 'header_flags', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of 2 letter DNS header flags.\n\nExpected values are: AA, TC, RD, RA, AD, CD, DO.', - example: ['RD', 'RA'], - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The DNS packet identifier assigned by the program that generated\nthe query. The identifier is copied to the response.', - example: 62111, - }, - { - name: 'op_code', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The DNS operation code that specifies the kind of query in the\nmessage. This value is set by the originator of a query and copied into the\nresponse.', - example: 'QUERY', - }, - { - name: 'question.class', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The class of records being queried.', - example: 'IN', - }, - { - name: 'question.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The name being queried.\n\nIf the name field contains non-printable characters (below 32 or above 126),\nthose characters should be represented as escaped base 10 integers (\\DDD).\nBack slashes and quotes should be escaped. Tabs, carriage returns, and line\nfeeds should be converted to \\t, \\r, and \\n respectively.', - example: 'www.google.com', - }, - { - name: 'question.registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'question.subdomain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The subdomain is all of the labels under the registered_domain.\n\nIf the domain has multiple levels of subdomain, such as "sub2.sub1.example.com",\nthe subdomain field should contain "sub2.sub1", with no trailing period.', - example: 'www', - }, - { - name: 'question.top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'question.type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The type of record being queried.', - example: 'AAAA', - }, - { - name: 'resolved_ip', - level: 'extended', - type: 'ip', - description: - 'Array containing all IPs seen in `answers.data`.\n\nThe `answers` array can be difficult to use, because of the variety of data\nformats it can contain. Extracting all IP addresses seen in there to `dns.resolved_ip`\nmakes it possible to index them as IP addresses, and makes them easier to\nvisualize and query for.', - example: ['10.10.10.10', '10.10.10.11'], - }, - { - name: 'response_code', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The DNS response code.', - example: 'NOERROR', - }, - { - name: 'type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of DNS event captured, query or answer.\n\nIf your source of DNS events only gives you DNS queries, you should only create\ndns events of type `dns.type:query`.\n\nIf your source of DNS events gives you answers as well, you should create\none event per query (optionally as soon as the query is seen). And a second\nevent containing all query details as well as an array of answers.', - example: 'answer', - }, - ], - }, - { - name: 'ecs', - title: 'ECS', - group: 2, - description: 'Meta-information specific to ECS.', - type: 'group', - fields: [ - { - name: 'version', - level: 'core', - required: true, - type: 'keyword', - ignore_above: 1024, - description: - 'ECS version this event conforms to. `ecs.version` is a required\nfield and must exist in all events.\n\nWhen querying across multiple indices -- which may conform to slightly different\nECS versions -- this field lets integrations adjust to the schema version\nof the events.', - example: '1.0.0', - }, - ], - }, - { - name: 'error', - title: 'Error', - group: 2, - description: - 'These fields can represent errors of any kind.\n\nUse them for errors that happen while fetching events or in cases where the\nevent itself contains an error.', - type: 'group', - fields: [ - { - name: 'code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Error code describing the error.', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the error.', - }, - { - name: 'message', - level: 'core', - type: 'text', - description: 'Error message.', - }, - { - name: 'stack_trace', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'The stack trace of this error in plain text.', - }, - { - name: 'type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The type of the error, for example the class name of the exception.', - example: 'java.lang.NullPointerException', - }, - ], - }, - { - name: 'event', - title: 'Event', - group: 2, - description: - 'The event fields are used for context information about the log\nor metric event itself.\n\nA log is defined as an event containing details of something that happened.\nLog events must include the time at which the thing happened. Examples of log\nevents include a process starting on a host, a network packet being sent from\na source to a destination, or a network connection between a client and a server\nbeing initiated or closed. A metric is defined as an event containing one or\nmore numerical measurements and the time at which the measurement was taken.\nExamples of metric events include memory pressure measured on a host and device\ntemperature. See the `event.kind` definition in this section for additional\ndetails about metric and state events.', - type: 'group', - fields: [ - { - name: 'action', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.', - example: 'user-password-change', - }, - { - name: 'category', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is one of four ECS Categorization Fields, and indicates the\nsecond level in the ECS category hierarchy.\n\n`event.category` represents the "big buckets" of ECS categories. For example,\nfiltering on `event.category:process` yields all events relating to process\nactivity. This field is closely related to `event.type`, which is used as\na subcategory.\n\nThis field is an array. This will allow proper categorization of some events\nthat fall in multiple categories.', - example: 'authentication', - }, - { - name: 'code', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Identification code for this event, if one exists.\n\nSome event sources use event codes to identify messages unambiguously, regardless\nof message language or wording adjustments over time. An example of this is\nthe Windows Event ID.', - example: 4648, - }, - { - name: 'created', - level: 'core', - type: 'date', - description: - 'event.created contains the date/time when the event was first\nread by an agent, or by your pipeline.\n\nThis field is distinct from @timestamp in that @timestamp typically contain\nthe time extracted from the original event.\n\nIn most situations, these two timestamps will be slightly different. The difference\ncan be used to calculate the delay between your source generating an event,\nand the time when your agent first processed it. This can be used to monitor\nyour agent or pipeline ability to keep up with your event source.\n\nIn case the two timestamps are identical, @timestamp should be used.', - example: '2016-05-23T08:05:34.857Z', - }, - { - name: 'dataset', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the dataset.\n\nIf an event source publishes more than one type of log or events (e.g. access\nlog, error log), the dataset is used to specify which one the event comes\nfrom.\n\nIt is recommended but not required to start the dataset name with the module\nname, followed by a dot, then the dataset name.', - example: 'apache.access', - }, - { - name: 'duration', - level: 'core', - type: 'long', - format: 'duration', - input_format: 'nanoseconds', - output_format: 'asMilliseconds', - output_precision: 1, - description: - 'Duration of the event in nanoseconds.\n\nIf event.start and event.end are known this value should be the difference\nbetween the end and start time.', - }, - { - name: 'end', - level: 'extended', - type: 'date', - description: - 'event.end contains the date when the event ended or when the activity\nwas last observed.', - }, - { - name: 'hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Hash (perhaps logstash fingerprint) of raw field to be able to\ndemonstrate log integrity.', - example: '123456789012345678901234567890ABCD', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique ID to describe the event.', - example: '8a4f500d', - }, - { - name: 'ingested', - level: 'core', - type: 'date', - description: - 'Timestamp when an event arrived in the central data store.\n\nThis is different from `@timestamp`, which is when the event originally occurred. It is\nalso different from `event.created`, which is meant to capture the first time\nan agent saw the event.\n\nIn normal conditions, assuming no tampering, the timestamps should chronologically\nlook like this: `@timestamp` < `event.created` < `event.ingested`.', - example: '2016-05-23T08:05:35.101Z', - default_field: false, - }, - { - name: 'kind', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is one of four ECS Categorization Fields, and indicates the\nhighest level in the ECS category hierarchy.\n\n`event.kind` gives high-level information about what type of information the\nevent contains, without being specific to the contents of the event. For example,\nvalues of this field distinguish alert events from metric events.\n\nThe value of this field can be used to inform how these kinds of events should\nbe handled. They may warrant different retention, different access control,\nit may also help understand whether the data coming in at a regular interval\nor not.', - example: 'alert', - }, - { - name: 'module', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the module this data is coming from.\n\nIf your monitoring agent supports the concept of modules or plugins to process\nevents of a given source (e.g. Apache logs), `event.module` should contain\nthe name of this module.', - example: 'apache', - }, - { - name: 'original', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Raw text message of entire event. Used to demonstrate log integrity.\n\nThis field is not indexed and doc_values are disabled. It cannot be searched,\nbut it can be retrieved from `_source`.', - example: - 'Sep 19 08:26:10 host CEF:0|Security| threatmanager|1.0|100|\nworm successfully stopped|10|src=10.0.0.1 dst=2.1.2.2spt=1232', - }, - { - name: 'outcome', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is one of four ECS Categorization Fields, and indicates the\nlowest level in the ECS category hierarchy.\n\n`event.outcome` simply denotes whether the event represents a success or a\nfailure from the perspective of the entity that produced the event.\n\nNote that when a single transaction is described in multiple events, each\nevent may populate different values of `event.outcome`, according to their\nperspective.\n\nAlso note that in the case of a compound event (a single event that contains\nmultiple logical events), this field should be populated with the value that\nbest captures the overall success or failure from the perspective of the event\nproducer.\n\nFurther note that not all events will have an associated outcome. For example,\nthis field is generally not populated for metric events, events with `event.type:info`,\nor any events for which an outcome does not make logical sense.', - example: 'success', - }, - { - name: 'provider', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Source of the event.\n\nEvent transports such as Syslog or the Windows Event Log typically mention\nthe source of an event. It can be the name of the software that generated\nthe event (e.g. Sysmon, httpd), or of a subsystem of the operating system\n(kernel, Microsoft-Windows-Security-Auditing).', - example: 'kernel', - }, - { - name: 'reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Reference URL linking to additional information about this event.\n\nThis URL links to a static definition of the this event. Alert events, indicated\nby `event.kind:alert`, are a common use case for this field.', - example: 'https://system.vendor.com/event/#0001234', - default_field: false, - }, - { - name: 'risk_score', - level: 'core', - type: 'float', - description: - "Risk score or priority of the event (e.g. security solutions).\nUse your system's original value here.", - }, - { - name: 'risk_score_norm', - level: 'extended', - type: 'float', - description: - 'Normalized risk score or priority of the event, on a scale of\n0 to 100.\n\nThis is mainly useful if you use more than one system that assigns risk scores,\nand you want to see a normalized value across all systems.', - }, - { - name: 'sequence', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Sequence number of the event.\n\nThe sequence number is a value published by some event sources, to make the\nexact ordering of events unambiguous, regardless of the timestamp precision.', - }, - { - name: 'severity', - level: 'core', - type: 'long', - format: 'string', - description: - 'The numeric severity of the event according to your event source.\n\nWhat the different severity values mean can be different between sources and\nuse cases. It is up to the implementer to make sure severities are consistent\nacross events from the same source.\n\nThe Syslog severity belongs in `log.syslog.severity.code`. `event.severity`\nis meant to represent the severity according to the event source (e.g. firewall,\nIDS). If the event source does not publish its own severity, you may optionally\ncopy the `log.syslog.severity.code` to `event.severity`.', - example: 7, - }, - { - name: 'start', - level: 'extended', - type: 'date', - description: - 'event.start contains the date when the event started or when the\nactivity was first observed.', - }, - { - name: 'timezone', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'This field should be populated when the event timestamp does\nnot include timezone information already (e.g. default Syslog timestamps).\nIt is optional otherwise.\n\nAcceptable timezone formats are: a canonical ID (e.g. "Europe/Amsterdam"),\nabbreviated (e.g. "EST") or an HH:mm differential (e.g. "-05:00").', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is one of four ECS Categorization Fields, and indicates the\nthird level in the ECS category hierarchy.\n\n`event.type` represents a categorization "sub-bucket" that, when used along\nwith the `event.category` field values, enables filtering events down to a\nlevel appropriate for single visualization.\n\nThis field is an array. This will allow proper categorization of some events\nthat fall in multiple event types.', - }, - { - name: 'url', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'URL linking to an external system to continue investigation of\nthis event.\n\nThis URL links to another system where in-depth investigation of the specific\noccurence of this event can take place. Alert events, indicated by `event.kind:alert`,\nare a common use case for this field.', - example: 'https://mysystem.mydomain.com/alert/5271dedb-f5b0-4218-87f0-4ac4870a38fe', - default_field: false, - }, - ], - }, - { - name: 'file', - title: 'File', - group: 2, - description: - 'A file is defined as a set of information that has been created\non, or has existed on a filesystem.\n\nFile objects can be associated with host events, network events, and/or file\nevents (e.g., those produced by File Integrity Monitoring [FIM] products or\nservices). File fields provide details about the affected file associated with\nthe event or metric.', - type: 'group', - fields: [ - { - name: 'accessed', - level: 'extended', - type: 'date', - description: - 'Last time the file was accessed.\n\nNote that not all filesystems keep track of access time.', - }, - { - name: 'attributes', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of file attributes.\n\nAttributes names will vary by platform. Here is a non-exhaustive list of values\nthat are expected in this field: archive, compressed, directory, encrypted,\nexecute, hidden, read, readonly, system, write.', - example: '["readonly", "system"]', - default_field: false, - }, - { - name: 'code_signature.exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'code_signature.subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'code_signature.trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - { - name: 'created', - level: 'extended', - type: 'date', - description: - 'File creation time.\n\nNote that not all filesystems store the creation time.', - }, - { - name: 'ctime', - level: 'extended', - type: 'date', - description: - 'Last time the file attributes or metadata changed.\n\nNote that changes to the file content will update `mtime`. This implies `ctime`\nwill be adjusted at the same time, since `mtime` is an attribute of the file.', - }, - { - name: 'device', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Device that is the source of the file.', - example: 'sda', - }, - { - name: 'directory', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Directory where the file is located. It should include the drive\nletter, when appropriate.', - example: '/home/alice', - }, - { - name: 'drive_letter', - level: 'extended', - type: 'keyword', - ignore_above: 1, - description: - 'Drive letter where the file is located. This field is only relevant\non Windows.\n\nThe value should be uppercase, and not include the colon.', - example: 'C', - default_field: false, - }, - { - name: 'extension', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'File extension.', - example: 'png', - }, - { - name: 'gid', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Primary group ID (GID) of the file.', - example: '1001', - }, - { - name: 'group', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Primary group name of the file.', - example: 'alice', - }, - { - name: 'hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - }, - { - name: 'hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - }, - { - name: 'hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - }, - { - name: 'hash.sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - }, - { - name: 'inode', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Inode representing the file in the filesystem.', - example: '256383', - }, - { - name: 'mime_type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'MIME type should identify the format of the file or stream of bytes\nusing https://www.iana.org/assignments/media-types/media-types.xhtml[IANA\nofficial types], where possible. When more than one type is applicable, the\nmost specific type should be used.', - default_field: false, - }, - { - name: 'mode', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Mode of the file in octal representation.', - example: '0640', - }, - { - name: 'mtime', - level: 'extended', - type: 'date', - description: 'Last time the file content was modified.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the file including the extension, without the directory.', - example: 'example.png', - }, - { - name: 'owner', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: "File owner's username.", - example: 'alice', - }, - { - name: 'path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'Full path to the file, including the file name. It should include\nthe drive letter, when appropriate.', - example: '/home/alice/example.png', - }, - { - name: 'pe.company', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal company name of the file, provided at compile-time.', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'pe.description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal description of the file, provided at compile-time.', - example: 'Paint', - default_field: false, - }, - { - name: 'pe.file_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal version of the file, provided at compile-time.', - example: '6.3.9600.17415', - default_field: false, - }, - { - name: 'pe.original_file_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal name of the file, provided at compile-time.', - example: 'MSPAINT.EXE', - default_field: false, - }, - { - name: 'pe.product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal product name of the file, provided at compile-time.', - example: 'Microsoft® Windows® Operating System', - default_field: false, - }, - { - name: 'size', - level: 'extended', - type: 'long', - description: 'File size in bytes.\n\nOnly relevant when `file.type` is "file".', - example: 16384, - }, - { - name: 'target_path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Target path for symlinks.', - }, - { - name: 'type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'File type (file, dir, or symlink).', - example: 'file', - }, - { - name: 'uid', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The user ID (UID) or security identifier (SID) of the file owner.', - example: '1001', - }, - ], - }, - { - name: 'geo', - title: 'Geo', - group: 2, - description: - 'Geo fields can carry data about a specific location related to an\nevent.\n\nThis geolocation information can be derived from techniques such as Geo IP,\nor be user-supplied.', - type: 'group', - fields: [ - { - name: 'city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - ], - }, - { - name: 'group', - title: 'Group', - group: 2, - description: - 'The group fields are meant to represent groups that are relevant\nto the event.', - type: 'group', - fields: [ - { - name: 'domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - ], - }, - { - name: 'hash', - title: 'Hash', - group: 2, - description: - 'The hash fields represent different hash algorithms and their values.\n\nField names for common hashes (e.g. MD5, SHA1) are predefined. Add fields for\nother hashes by lowercasing the hash algorithm name and using underscore separators\nas appropriate (snake case, e.g. sha3_512).', - type: 'group', - fields: [ - { - name: 'md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - }, - { - name: 'sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - }, - { - name: 'sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - }, - { - name: 'sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - }, - ], - }, - { - name: 'host', - title: 'Host', - group: 2, - description: - 'A host is defined as a general computing instance.\n\nECS host.* fields should be populated with details about the host on which the\nevent happened, or from which the measurement was taken. Host types include\nhardware, virtual machines, Docker containers, and Kubernetes nodes.', - type: 'group', - fields: [ - { - name: 'architecture', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system architecture.', - example: 'x86_64', - }, - { - name: 'domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the domain of which the host is a member.\n\nFor example, on Windows this could be the host Active Directory domain\nor NetBIOS domain name. For Linux this could be the domain of the host\nLDAP provider.', - example: 'CONTOSO', - default_field: false, - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'hostname', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Hostname of the host.\n\nIt normally contains what the `hostname` command returns on the host machine.', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique host id.\n\nAs hostname is not always unique, use values that are meaningful in your environment.\n\nExample: The current usage of `beat.name`.', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: 'Host ip addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Host mac addresses.', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.', - }, - { - name: 'os.family', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'os.full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'os.kernel', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'os.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'os.platform', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'os.version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Type of host.\n\nFor Cloud providers this can be the machine type like `t2.medium`. If vm,\nthis could be the container, for example, or other information meaningful\nin your environment.', - }, - { - name: 'uptime', - level: 'extended', - type: 'long', - description: 'Seconds the host has been up.', - example: 1325, - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'http', - title: 'HTTP', - group: 2, - description: - 'Fields related to HTTP activity. Use the `url` field set to store\nthe url of the request.', - type: 'group', - fields: [ - { - name: 'request.body.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Size in bytes of the request body.', - example: 887, - }, - { - name: 'request.body.content', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'The full HTTP request body.', - example: 'Hello world', - }, - { - name: 'request.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Total size in bytes of the request (body and headers).', - example: 1437, - }, - { - name: 'request.method', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'HTTP request method.\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'get, post, put', - }, - { - name: 'request.referrer', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Referrer for this HTTP request.', - example: 'https://blog.example.com/', - }, - { - name: 'response.body.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Size in bytes of the response body.', - example: 887, - }, - { - name: 'response.body.content', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'The full HTTP response body.', - example: 'Hello world', - }, - { - name: 'response.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Total size in bytes of the response (body and headers).', - example: 1437, - }, - { - name: 'response.status_code', - level: 'extended', - type: 'long', - format: 'string', - description: 'HTTP response status code.', - example: 404, - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'HTTP version.', - example: 1.1, - }, - ], - }, - { - name: 'interface', - title: 'Interface', - group: 2, - description: - 'The interface fields are used to record ingress and egress interface\ninformation when reported by an observer (e.g. firewall, router, load balancer)\nin the context of the observer handling a network connection. In the case of\na single observer interface (e.g. network sensor on a span port) only the observer.ingress\ninformation should be populated.', - type: 'group', - fields: [ - { - name: 'alias', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Interface alias as reported by the system, typically used in firewall\nimplementations for e.g. inside, outside, or dmz logical interface naming.', - example: 'outside', - default_field: false, - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface ID as reported by an observer (typically SNMP interface\nID).', - example: 10, - default_field: false, - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface name as reported by the system.', - example: 'eth0', - default_field: false, - }, - ], - }, - { - name: 'log', - title: 'Log', - group: 2, - description: - 'Details about the event logging mechanism or logging transport.\n\nThe log.* fields are typically populated with details about the logging mechanism\nused to create and/or transport the event. For example, syslog details belong\nunder `log.syslog.*`.\n\nThe details specific to your event source are typically not logged under `log.*`,\nbut rather in `event.*` or in other ECS fields.', - type: 'group', - fields: [ - { - name: 'level', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Original log level of the log event.\n\nIf the source of the event provides a log level or textual severity, this\nis the one that goes in `log.level`. If your source does not specify one,\nyou may put your event transport severity here (e.g. Syslog severity).\n\nSome examples are `warn`, `err`, `i`, `informational`.', - example: 'error', - }, - { - name: 'logger', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'The name of the logger inside an application. This is usually the\nname of the class which initialized the logger, or can be a custom name.', - example: 'org.elasticsearch.bootstrap.Bootstrap', - }, - { - name: 'origin.file.line', - level: 'extended', - type: 'integer', - description: - 'The line number of the file containing the source code which originated\nthe log event.', - example: 42, - }, - { - name: 'origin.file.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The name of the file containing the source code which originated\nthe log event. Note that this is not the name of the log file.', - example: 'Bootstrap.java', - }, - { - name: 'origin.function', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The name of the function or method which originated the log event.', - example: 'init', - }, - { - name: 'original', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'This is the original log message and contains the full log message\nbefore splitting it up in multiple parts.\n\nIn contrast to the `message` field which can contain an extracted part of\nthe log message, this field contains the original, full log message. It can\nhave already some modifications applied like encoding or new lines removed\nto clean up the log message.\n\nThis field is not indexed and doc_values are disabled so it can not be queried\nbut the value can be retrieved from `_source`.', - example: 'Sep 19 08:26:10 localhost My log', - }, - { - name: 'syslog', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'The Syslog metadata of the event, if the event was transmitted\nvia Syslog. Please see RFCs 5424 or 3164.', - }, - { - name: 'syslog.facility.code', - level: 'extended', - type: 'long', - format: 'string', - description: - 'The Syslog numeric facility of the log event, if available.\n\nAccording to RFCs 5424 and 3164, this value should be an integer between 0\nand 23.', - example: 23, - }, - { - name: 'syslog.facility.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The Syslog text-based facility of the log event, if available.', - example: 'local7', - }, - { - name: 'syslog.priority', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Syslog numeric priority of the event, if available.\n\nAccording to RFCs 5424 and 3164, the priority is 8 * facility + severity.\nThis number is therefore expected to contain a value between 0 and 191.', - example: 135, - }, - { - name: 'syslog.severity.code', - level: 'extended', - type: 'long', - description: - 'The Syslog numeric severity of the log event, if available.\n\nIf the event source publishing via Syslog provides a different numeric severity\nvalue (e.g. firewall, IDS), your source numeric severity should go to `event.severity`.\nIf the event source does not specify a distinct severity, you can optionally\ncopy the Syslog severity to `event.severity`.', - example: 3, - }, - { - name: 'syslog.severity.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The Syslog numeric severity of the log event, if available.\n\nIf the event source publishing via Syslog provides a different severity value\n(e.g. firewall, IDS), your source text severity should go to `log.level`.\nIf the event source does not specify a distinct severity, you can optionally\ncopy the Syslog severity to `log.level`.', - example: 'Error', - }, - ], - }, - { - name: 'network', - title: 'Network', - group: 2, - description: - 'The network is defined as the communication path over which a host\nor network event happens.\n\nThe network.* fields should be populated with details about the network activity\nassociated with an event.', - type: 'group', - fields: [ - { - name: 'application', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A name given to an application level protocol. This can be arbitrarily\nassigned for things like microservices, but also apply to things like skype,\nicq, facebook, twitter. This would be used in situations where the vendor\nor service can be decoded such as from the source/dest IP owners, ports, or\nwire format.\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'aim', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: - 'Total bytes transferred in both directions.\n\nIf `source.bytes` and `destination.bytes` are known, `network.bytes` is their\nsum.', - example: 368, - }, - { - name: 'community_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A hash of source and destination IPs and ports, as well as the\nprotocol used in a communication. This is a tool-agnostic standard to identify\nflows.\n\nLearn more at https://github.com/corelight/community-id-spec.', - example: '1:hO+sN4H+MG5MY/8hIrXPqc4ZQz0=', - }, - { - name: 'direction', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - "Direction of the network traffic.\nRecommended values are:\n * inbound\n * outbound\n * internal\n * external\n * unknown\n\nWhen mapping events from a host-based monitoring context, populate this field from the host's point of view.\nWhen mapping events from a network or perimeter-based monitoring context, populate this field from the point of view of your network perimeter.", - example: 'inbound', - }, - { - name: 'forwarded_ip', - level: 'core', - type: 'ip', - description: 'Host IP address when the source IP address is the proxy.', - example: '192.1.1.2', - }, - { - name: 'iana_number', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'IANA Protocol Number (https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml).\nStandardized list of protocols. This aligns well with NetFlow and sFlow related\nlogs which use the IANA Protocol Number.', - example: 6, - }, - { - name: 'inner', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'Network.inner fields are added in addition to network.vlan fields\nto describe the innermost VLAN when q-in-q VLAN tagging is present. Allowed\nfields include vlan.id and vlan.name. Inner vlan fields are typically used\nwhen sending traffic with multiple 802.1q encapsulations to a network sensor\n(e.g. Zeek, Wireshark.)', - default_field: false, - }, - { - name: 'inner.vlan.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'inner.vlan.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name given by operators to sections of their network.', - example: 'Guest Wifi', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: - 'Total packets transferred in both directions.\n\nIf `source.packets` and `destination.packets` are known, `network.packets`\nis their sum.', - example: 24, - }, - { - name: 'protocol', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'L7 Network protocol name. ex. http, lumberjack, transport protocol.\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'http', - }, - { - name: 'transport', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Same as network.iana_number, but instead using the Keyword name\nof the transport layer (udp, tcp, ipv6-icmp, etc.)\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'tcp', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'In the OSI Model this would be the Network Layer. ipv4, ipv6,\nipsec, pim, etc\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'ipv4', - }, - { - name: 'vlan.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'vlan.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - ], - }, - { - name: 'observer', - title: 'Observer', - group: 2, - description: - 'An observer is defined as a special network, security, or application\ndevice used to detect, observe, or create network, security, or application-related\nevents and metrics.\n\nThis could be a custom hardware appliance or a server that has been configured\nto run special network, security, or application software. Examples include\nfirewalls, web proxies, intrusion detection/prevention systems, network monitoring\nsensors, web application firewalls, data loss prevention systems, and APM servers.\nThe observer.* fields shall be populated with details of the system, if any,\nthat detects, observes and/or creates a network, security, or application event\nor metric. Message queues and ETL components used in processing events or metrics\nare not considered observers in ECS.', - type: 'group', - fields: [ - { - name: 'egress', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'Observer.egress holds information like interface number and name,\nvlan, and zone information to classify egress traffic. Single armed monitoring\nsuch as a network sensor on a span port should only use observer.ingress\nto categorize traffic.', - default_field: false, - }, - { - name: 'egress.interface.alias', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Interface alias as reported by the system, typically used in firewall\nimplementations for e.g. inside, outside, or dmz logical interface naming.', - example: 'outside', - default_field: false, - }, - { - name: 'egress.interface.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface ID as reported by an observer (typically SNMP interface\nID).', - example: 10, - default_field: false, - }, - { - name: 'egress.interface.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface name as reported by the system.', - example: 'eth0', - default_field: false, - }, - { - name: 'egress.vlan.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'egress.vlan.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - { - name: 'egress.zone', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Network zone of outbound traffic as reported by the observer to\ncategorize the destination area of egress traffic, e.g. Internal, External,\nDMZ, HR, Legal, etc.', - example: 'Public_Internet', - default_field: false, - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'hostname', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Hostname of the observer.', - }, - { - name: 'ingress', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: - 'Observer.ingress holds information like interface number and name,\nvlan, and zone information to classify ingress traffic. Single armed monitoring\nsuch as a network sensor on a span port should only use observer.ingress\nto categorize traffic.', - default_field: false, - }, - { - name: 'ingress.interface.alias', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Interface alias as reported by the system, typically used in firewall\nimplementations for e.g. inside, outside, or dmz logical interface naming.', - example: 'outside', - default_field: false, - }, - { - name: 'ingress.interface.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface ID as reported by an observer (typically SNMP interface\nID).', - example: 10, - default_field: false, - }, - { - name: 'ingress.interface.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Interface name as reported by the system.', - example: 'eth0', - default_field: false, - }, - { - name: 'ingress.vlan.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'ingress.vlan.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - { - name: 'ingress.zone', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Network zone of incoming traffic as reported by the observer to\ncategorize the source area of ingress traffic. e.g. internal, External, DMZ,\nHR, Legal, etc.', - example: 'DMZ', - default_field: false, - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: 'IP addresses of the observer.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC addresses of the observer', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Custom name of the observer.\n\nThis is a name that can be given to an observer. This can be helpful for example\nif multiple firewalls of the same model are used in an organization.\n\nIf no custom name is needed, the field can be left empty.', - example: '1_proxySG', - }, - { - name: 'os.family', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'os.full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'os.kernel', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'os.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'os.platform', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'os.version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - { - name: 'product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The product name of the observer.', - example: 's200', - }, - { - name: 'serial_number', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Observer serial number.', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of the observer the data is coming from.\n\nThere is no predefined list of observer types. Some examples are `forwarder`,\n`firewall`, `ids`, `ips`, `proxy`, `poller`, `sensor`, `APM server`.', - example: 'firewall', - }, - { - name: 'vendor', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Vendor name of the observer.', - example: 'Symantec', - }, - { - name: 'version', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Observer version.', - }, - ], - }, - { - name: 'organization', - title: 'Organization', - group: 2, - description: - 'The organization fields enrich data with information about the company\nor entity the data is associated with.\n\nThese fields help you arrange or filter data stored in an index by one or multiple\norganizations.', - type: 'group', - fields: [ - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the organization.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - }, - ], - }, - { - name: 'os', - title: 'Operating System', - group: 2, - description: 'The OS fields contain information about the operating system.', - type: 'group', - fields: [ - { - name: 'family', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'kernel', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'platform', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - ], - }, - { - name: 'package', - title: 'Package', - group: 2, - description: - 'These fields contain information about an installed software package.\nIt contains general information about a package, such as name, version or size.\nIt also contains installation details, such as time or location.', - type: 'group', - fields: [ - { - name: 'architecture', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Package architecture.', - example: 'x86_64', - }, - { - name: 'build_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the build version of the installed\npackage.\n\nFor example use the commit SHA of a non-released package.', - example: '36f4f7e89dd61b0988b12ee000b98966867710cd', - default_field: false, - }, - { - name: 'checksum', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Checksum of the installed package for verification.', - example: '68b329da9893e34099c7d8ad5cb9c940', - }, - { - name: 'description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Description of the package.', - example: - 'Open source programming language to build simple/reliable/efficient\nsoftware.', - }, - { - name: 'install_scope', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Indicating how the package was installed, e.g. user-local, global.', - example: 'global', - }, - { - name: 'installed', - level: 'extended', - type: 'date', - description: 'Time when package was installed.', - }, - { - name: 'license', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'License under which the package was released.\n\nUse a short name, e.g. the license identifier from SPDX License List where\npossible (https://spdx.org/licenses/).', - example: 'Apache License 2.0', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Package name', - example: 'go', - }, - { - name: 'path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Path where the package is installed.', - example: '/usr/local/Cellar/go/1.12.9/', - }, - { - name: 'reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Home page or reference URL of the software in this package, if\navailable.', - example: 'https://golang.org', - default_field: false, - }, - { - name: 'size', - level: 'extended', - type: 'long', - format: 'string', - description: 'Package size in bytes.', - example: 62231, - }, - { - name: 'type', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Type of package.\n\nThis should contain the package file type, rather than the package manager\nname. Examples: rpm, dpkg, brew, npm, gem, nupkg, jar.', - example: 'rpm', - default_field: false, - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Package version', - example: '1.12.9', - }, - ], - }, - { - name: 'pe', - title: 'PE Header', - group: 2, - description: 'These fields contain Windows Portable Executable (PE) metadata.', - type: 'group', - fields: [ - { - name: 'company', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal company name of the file, provided at compile-time.', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal description of the file, provided at compile-time.', - example: 'Paint', - default_field: false, - }, - { - name: 'file_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal version of the file, provided at compile-time.', - example: '6.3.9600.17415', - default_field: false, - }, - { - name: 'original_file_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal name of the file, provided at compile-time.', - example: 'MSPAINT.EXE', - default_field: false, - }, - { - name: 'product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal product name of the file, provided at compile-time.', - example: 'Microsoft® Windows® Operating System', - default_field: false, - }, - ], - }, - { - name: 'process', - title: 'Process', - group: 2, - description: - 'These fields contain information about a process.\n\nThese fields can help you correlate metrics information with a process id/name\nfrom a log message. The `process.pid` often stays in the metric itself and\nis copied to the global field for correlation.', - type: 'group', - fields: [ - { - name: 'args', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of process arguments, starting with the absolute path to\nthe executable.\n\nMay be filtered to protect sensitive information.', - example: ['/usr/bin/ssh', '-l', 'user', '10.0.0.16'], - }, - { - name: 'args_count', - level: 'extended', - type: 'long', - description: - 'Length of the process.args array.\n\nThis field can be useful for querying or performing bucket analysis on how\nmany arguments were provided to start a process. More arguments may be an\nindication of suspicious activity.', - example: 4, - default_field: false, - }, - { - name: 'code_signature.exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'code_signature.subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'code_signature.trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'code_signature.valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - { - name: 'command_line', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: - 'Full command line that started the process, including the absolute\npath to the executable, and all arguments.\n\nSome arguments may be filtered to protect sensitive information.', - example: '/usr/bin/ssh -l user 10.0.0.16', - default_field: false, - }, - { - name: 'entity_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier for the process.\n\nThe implementation of this is specified by the data source, but some examples\nof what could be used here are a process-generated UUID, Sysmon Process GUIDs,\nor a hash of some uniquely identifying components of a process.\n\nConstructing a globally unique identifier is a common practice to mitigate\nPID reuse as well as to identify a specific process over time, across multiple\nmonitored hosts.', - example: 'c2c455d9f99375d', - default_field: false, - }, - { - name: 'executable', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Absolute path to the process executable.', - example: '/usr/bin/ssh', - }, - { - name: 'exit_code', - level: 'extended', - type: 'long', - description: - 'The exit code of the process, if this is a termination event.\n\nThe field should be absent if there is no exit code for the event (e.g. process\nstart).', - example: 137, - default_field: false, - }, - { - name: 'hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - }, - { - name: 'hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - }, - { - name: 'hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - }, - { - name: 'hash.sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Process name.\n\nSometimes called program name or similar.', - example: 'ssh', - }, - { - name: 'parent.args', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of process arguments.\n\nMay be filtered to protect sensitive information.', - example: ['ssh', '-l', 'user', '10.0.0.16'], - default_field: false, - }, - { - name: 'parent.args_count', - level: 'extended', - type: 'long', - description: - 'Length of the process.args array.\n\nThis field can be useful for querying or performing bucket analysis on how\nmany arguments were provided to start a process. More arguments may be an\nindication of suspicious activity.', - example: 4, - default_field: false, - }, - { - name: 'parent.code_signature.exists', - level: 'core', - type: 'boolean', - description: 'Boolean to capture if a signature is present.', - example: 'true', - default_field: false, - }, - { - name: 'parent.code_signature.status', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Additional information about the certificate status.\n\nThis is useful for logging cryptographic errors with the certificate validity\nor trust status. Leave unpopulated if the validity or trust of the certificate\nwas unchecked.', - example: 'ERROR_UNTRUSTED_ROOT', - default_field: false, - }, - { - name: 'parent.code_signature.subject_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Subject name of the code signer', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'parent.code_signature.trusted', - level: 'extended', - type: 'boolean', - description: - 'Stores the trust status of the certificate chain.\n\nValidating the trust of the certificate chain may be complicated, and this\nfield should only be populated by tools that actively check the status.', - example: 'true', - default_field: false, - }, - { - name: 'parent.code_signature.valid', - level: 'extended', - type: 'boolean', - description: - 'Boolean to capture if the digital signature is verified against\nthe binary content.\n\nLeave unpopulated if a certificate was unchecked.', - example: 'true', - default_field: false, - }, - { - name: 'parent.command_line', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: - 'Full command line that started the process, including the absolute\npath to the executable, and all arguments.\n\nSome arguments may be filtered to protect sensitive information.', - example: '/usr/bin/ssh -l user 10.0.0.16', - default_field: false, - }, - { - name: 'parent.entity_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier for the process.\n\nThe implementation of this is specified by the data source, but some examples\nof what could be used here are a process-generated UUID, Sysmon Process GUIDs,\nor a hash of some uniquely identifying components of a process.\n\nConstructing a globally unique identifier is a common practice to mitigate\nPID reuse as well as to identify a specific process over time, across multiple\nmonitored hosts.', - example: 'c2c455d9f99375d', - default_field: false, - }, - { - name: 'parent.executable', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: 'Absolute path to the process executable.', - example: '/usr/bin/ssh', - default_field: false, - }, - { - name: 'parent.exit_code', - level: 'extended', - type: 'long', - description: - 'The exit code of the process, if this is a termination event.\n\nThe field should be absent if there is no exit code for the event (e.g. process\nstart).', - example: 137, - default_field: false, - }, - { - name: 'parent.hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'MD5 hash.', - default_field: false, - }, - { - name: 'parent.hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA1 hash.', - default_field: false, - }, - { - name: 'parent.hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA256 hash.', - default_field: false, - }, - { - name: 'parent.hash.sha512', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'SHA512 hash.', - default_field: false, - }, - { - name: 'parent.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: 'Process name.\n\nSometimes called program name or similar.', - example: 'ssh', - default_field: false, - }, - { - name: 'parent.pgid', - level: 'extended', - type: 'long', - format: 'string', - description: 'Identifier of the group of processes the process belongs to.', - default_field: false, - }, - { - name: 'parent.pid', - level: 'core', - type: 'long', - format: 'string', - description: 'Process id.', - example: 4242, - default_field: false, - }, - { - name: 'parent.ppid', - level: 'extended', - type: 'long', - format: 'string', - description: "Parent process' pid.", - example: 4241, - default_field: false, - }, - { - name: 'parent.start', - level: 'extended', - type: 'date', - description: 'The time the process started.', - example: '2016-05-23T08:05:34.853Z', - default_field: false, - }, - { - name: 'parent.thread.id', - level: 'extended', - type: 'long', - format: 'string', - description: 'Thread ID.', - example: 4242, - default_field: false, - }, - { - name: 'parent.thread.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Thread name.', - example: 'thread-0', - default_field: false, - }, - { - name: 'parent.title', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: - 'Process title.\n\nThe proctitle, some times the same as process name. Can also be different:\nfor example a browser setting its title to the web page currently opened.', - default_field: false, - }, - { - name: 'parent.uptime', - level: 'extended', - type: 'long', - description: 'Seconds the process has been up.', - example: 1325, - default_field: false, - }, - { - name: 'parent.working_directory', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: 'The working directory of the process.', - example: '/home/alice', - default_field: false, - }, - { - name: 'pe.company', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal company name of the file, provided at compile-time.', - example: 'Microsoft Corporation', - default_field: false, - }, - { - name: 'pe.description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal description of the file, provided at compile-time.', - example: 'Paint', - default_field: false, - }, - { - name: 'pe.file_version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal version of the file, provided at compile-time.', - example: '6.3.9600.17415', - default_field: false, - }, - { - name: 'pe.original_file_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal name of the file, provided at compile-time.', - example: 'MSPAINT.EXE', - default_field: false, - }, - { - name: 'pe.product', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Internal product name of the file, provided at compile-time.', - example: 'Microsoft® Windows® Operating System', - default_field: false, - }, - { - name: 'pgid', - level: 'extended', - type: 'long', - format: 'string', - description: 'Identifier of the group of processes the process belongs to.', - }, - { - name: 'pid', - level: 'core', - type: 'long', - format: 'string', - description: 'Process id.', - example: 4242, - }, - { - name: 'ppid', - level: 'extended', - type: 'long', - format: 'string', - description: "Parent process' pid.", - example: 4241, - }, - { - name: 'start', - level: 'extended', - type: 'date', - description: 'The time the process started.', - example: '2016-05-23T08:05:34.853Z', - }, - { - name: 'thread.id', - level: 'extended', - type: 'long', - format: 'string', - description: 'Thread ID.', - example: 4242, - }, - { - name: 'thread.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Thread name.', - example: 'thread-0', - }, - { - name: 'title', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'Process title.\n\nThe proctitle, some times the same as process name. Can also be different:\nfor example a browser setting its title to the web page currently opened.', - }, - { - name: 'uptime', - level: 'extended', - type: 'long', - description: 'Seconds the process has been up.', - example: 1325, - }, - { - name: 'working_directory', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'The working directory of the process.', - example: '/home/alice', - }, - ], - }, - { - name: 'registry', - title: 'Registry', - group: 2, - description: 'Fields related to Windows Registry operations.', - type: 'group', - fields: [ - { - name: 'data.bytes', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Original bytes written with base64 encoding.\n\nFor Windows registry operations, such as SetValueEx and RegQueryValueEx, this\ncorresponds to the data pointed by `lp_data`. This is optional but provides\nbetter recoverability and should be populated for REG_BINARY encoded values.', - example: 'ZQBuAC0AVQBTAAAAZQBuAAAAAAA=', - default_field: false, - }, - { - name: 'data.strings', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Content when writing string types.\n\nPopulated as an array when writing string data to the registry. For single\nstring registry types (REG_SZ, REG_EXPAND_SZ), this should be an array with\none string. For sequences of string with REG_MULTI_SZ, this array will be\nvariable length. For numeric data, such as REG_DWORD and REG_QWORD, this should\nbe populated with the decimal representation (e.g `"1"`).', - example: '["C:\\rta\\red_ttp\\bin\\myapp.exe"]', - default_field: false, - }, - { - name: 'data.type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Standard registry type for encoding contents', - example: 'REG_SZ', - default_field: false, - }, - { - name: 'hive', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Abbreviated name for the hive.', - example: 'HKLM', - default_field: false, - }, - { - name: 'key', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Hive-relative path of keys.', - example: - 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\winword.exe', - default_field: false, - }, - { - name: 'path', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Full path, including hive, key and value', - example: - 'HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution\nOptions\\winword.exe\\Debugger', - default_field: false, - }, - { - name: 'value', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the value written.', - example: 'Debugger', - default_field: false, - }, - ], - }, - { - name: 'related', - title: 'Related', - group: 2, - description: - 'This field set is meant to facilitate pivoting around a piece of\ndata.\n\nSome pieces of information can be seen in many places in an ECS event. To facilitate\nsearching for them, store an array of all seen values to their corresponding\nfield in `related.`.\n\nA concrete example is IP addresses, which can be under host, observer, source,\ndestination, client, server, and network.forwarded_ip. If you append all IPs\nto `related.ip`, you can then search for a given IP trivially, no matter where\nit appeared, by querying `related.ip:192.0.2.15`.', - type: 'group', - fields: [ - { - name: 'hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - "All the hashes seen on your event. Populating this field, then\nusing it to search for hashes can help in situations where you're unsure what\nthe hash algorithm is (and therefore which key name to search).", - default_field: false, - }, - { - name: 'ip', - level: 'extended', - type: 'ip', - description: 'All of the IPs seen on your event.', - }, - { - name: 'user', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'All the user names seen on your event.', - default_field: false, - }, - ], - }, - { - name: 'rule', - title: 'Rule', - group: 2, - description: - 'Rule fields are used to capture the specifics of any observer or\nagent rules that generate alerts or other notable events.\n\nExamples of data sources that would populate the rule fields include: network\nadmission control platforms, network or host IDS/IPS, network firewalls, web\napplication firewalls, url filters, endpoint detection and response (EDR) systems,\netc.', - type: 'group', - fields: [ - { - name: 'author', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name, organization, or pseudonym of the author or authors who created\nthe rule used to generate this event.', - example: ['Star-Lord'], - default_field: false, - }, - { - name: 'category', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A categorization value keyword used by the entity using the rule\nfor detection of this event.', - example: 'Attempted Information Leak', - default_field: false, - }, - { - name: 'description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The description of the rule generating the event.', - example: 'Block requests to public DNS over HTTPS / TLS protocols', - default_field: false, - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A rule ID that is unique within the scope of an agent, observer,\nor other entity using the rule for detection of this event.', - example: 101, - default_field: false, - }, - { - name: 'license', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the license under which the rule used to generate this\nevent is made available.', - example: 'Apache 2.0', - default_field: false, - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The name of the rule or signature generating the event.', - example: 'BLOCK_DNS_over_TLS', - default_field: false, - }, - { - name: 'reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Reference URL to additional information about the rule used to\ngenerate this event.\n\nThe URL can point to the vendor documentation about the rule. If that is\nnot available, it can also be a link to a more general page describing this\ntype of alert.', - example: 'https://en.wikipedia.org/wiki/DNS_over_TLS', - default_field: false, - }, - { - name: 'ruleset', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the ruleset, policy, group, or parent category in which\nthe rule used to generate this event is a member.', - example: 'Standard_Protocol_Filters', - default_field: false, - }, - { - name: 'uuid', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A rule ID that is unique within the scope of a set or group of\nagents, observers, or other entities using the rule for detection of this\nevent.', - example: 1100110011, - default_field: false, - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The version / revision of the rule being used for analysis.', - example: 1.1, - default_field: false, - }, - ], - }, - { - name: 'server', - title: 'Server', - group: 2, - description: - 'A Server is defined as the responder in a network connection for\nevents regarding sessions, connections, or bidirectional flow records.\n\nFor TCP events, the server is the receiver of the initial SYN packet(s) of the\nTCP connection. For other protocols, the server is generally the responder in\nthe network transaction. Some systems actually use the term "responder" to refer\nthe server in TCP connections. The server fields describe details about the\nsystem acting as the server in the network event. Server fields are usually\npopulated in conjunction with client fields. Server fields are generally not\npopulated for packet-level events.\n\nClient / server representations can add semantic context to an exchange, which\nis helpful to visualize the data in certain situations. If your context falls\nin that category, you should still ensure that source and destination are filled\nappropriately.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Some event server addresses are defined ambiguously. The event\nwill sometimes list an IP, a domain or a unix socket. You should always store\nthe raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'as.number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'as.organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the server to the client.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Server domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: - 'IP address of the server.\n\nCan be one or multiple IPv4 or IPv6 addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC address of the server.', - }, - { - name: 'nat.ip', - level: 'extended', - type: 'ip', - description: - 'Translated ip of destination based NAT sessions (e.g. internet\nto private DMZ)\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'nat.port', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Translated port of destination based NAT sessions (e.g. internet\nto private DMZ)\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the server to the client.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the server.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered server domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'service', - title: 'Service', - group: 2, - description: - 'The service fields describe the service for or from which the data\nwas collected.\n\nThese fields help you find and correlate logs for a specific service and version.', - type: 'group', - fields: [ - { - name: 'ephemeral_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Ephemeral identifier of this service (if one exists).\n\nThis id normally changes across restarts, but `service.id` does not.', - example: '8a4f500f', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier of the running service. If the service is comprised\nof many nodes, the `service.id` should be the same for all nodes.\n\nThis id should uniquely identify the service. This makes it possible to correlate\nlogs and metrics for one specific service, no matter which particular node\nemitted the event.\n\nNote that if you need to see the events from one specific host of the service,\nyou should filter on that `host.name` or `host.id` instead.', - example: 'd37e5ebfe0ae6c4972dbe9f0174a1637bb8247f6', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the service data is collected from.\n\nThe name of the service is normally user given. This allows for distributed\nservices that run on multiple hosts to correlate the related instances based\non the name.\n\nIn the case of Elasticsearch the `service.name` could contain the cluster\nname. For Beats the `service.name` is by default a copy of the `service.type`\nfield if no name is specified.', - example: 'elasticsearch-metrics', - }, - { - name: 'node.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of a service node.\n\nThis allows for two nodes of the same service running on the same host to\nbe differentiated. Therefore, `service.node.name` should typically be unique\nacross nodes of a given service.\n\nIn the case of Elasticsearch, the `service.node.name` could contain the unique\nnode name within the Elasticsearch cluster. In cases where the service doe not\nhave the concept of a node name, the host name or container name can be used\nto distinguish running instances that make up this service. If those do not\nprovide uniqueness (e.g. multiple instances of the service running on the\nsame host) - the node name can be manually set.', - example: 'instance-0000000016', - }, - { - name: 'state', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Current state of the service.', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of the service data is collected from.\n\nThe type can be used to group and correlate logs and metrics from one service\ntype.\n\nExample: If logs or metrics are collected from Elasticsearch, `service.type`\nwould be `elasticsearch`.', - example: 'elasticsearch', - }, - { - name: 'version', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: - 'Version of the service the data was collected from.\n\nThis allows to look at a data set only for a specific version of a service.', - example: '3.2.4', - }, - ], - }, - { - name: 'source', - title: 'Source', - group: 2, - description: - 'Source fields describe details about the source of a packet/event.\n\nSource fields are usually populated in conjunction with destination fields.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Some event source addresses are defined ambiguously. The event\nwill sometimes list an IP, a domain or a unix socket. You should always store\nthe raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'as.number', - level: 'extended', - type: 'long', - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - }, - { - name: 'as.organization.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Organization name.', - example: 'Google LLC', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the source to the destination.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Source domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: - 'IP address of the source.\n\nCan be one or multiple IPv4 or IPv6 addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'MAC address of the source.', - }, - { - name: 'nat.ip', - level: 'extended', - type: 'ip', - description: - 'Translated ip of source based NAT sessions (e.g. internal client\nto internet)\n\nTypically connections traversing load balancers, firewalls, or routers.', - }, - { - name: 'nat.port', - level: 'extended', - type: 'long', - format: 'string', - description: - 'Translated port of source based NAT sessions. (e.g. internal client\nto internet)\n\nTypically used with load balancers, firewalls, or routers.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the source to the destination.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the source.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered source domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'user.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'threat', - title: 'Threat', - group: 2, - description: - 'Fields to classify events and alerts according to a threat taxonomy\nsuch as the Mitre ATT&CK framework.\n\nThese fields are for users to classify alerts from all of their sources (e.g.\nIDS, NGFW, etc.) within a common taxonomy. The threat.tactic.* are meant to\ncapture the high level category of the threat (e.g. "impact"). The threat.technique.*\nfields are meant to capture which kind of approach is used by this detected\nthreat, to accomplish the goal (e.g. "endpoint denial of service").', - type: 'group', - fields: [ - { - name: 'framework', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the threat framework used to further categorize and classify\nthe tactic and technique of the reported threat. Framework classification\ncan be provided by detecting systems, evaluated at ingest time, or retrospectively\ntagged to events.', - example: 'MITRE ATT&CK', - }, - { - name: 'tactic.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The id of tactic used by this threat. You can use the Mitre ATT&CK\nMatrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/\n)', - example: 'TA0040', - }, - { - name: 'tactic.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the type of tactic used by this threat. You can use the\nMitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/\n)', - example: 'impact', - }, - { - name: 'tactic.reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The reference url of tactic used by this threat. You can use the\nMitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/\n)', - example: 'https://attack.mitre.org/tactics/TA0040/', - }, - { - name: 'technique.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The id of technique used by this tactic. You can use the Mitre\nATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/\n)', - example: 'T1499', - }, - { - name: 'technique.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'The name of technique used by this tactic. You can use the Mitre\nATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/\n)', - example: 'endpoint denial of service', - }, - { - name: 'technique.reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The reference url of technique used by this tactic. You can use\nthe Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/\n)', - example: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - { - name: 'tls', - title: 'TLS', - group: 2, - description: - 'Fields related to a TLS connection. These fields focus on the TLS\nprotocol itself and intentionally avoids in-depth analysis of the related x.509\ncertificate files.', - type: 'group', - fields: [ - { - name: 'cipher', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'String indicating the cipher used during the current connection.', - example: 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256', - default_field: false, - }, - { - name: 'client.certificate', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'PEM-encoded stand-alone certificate offered by the client. This\nis usually mutually-exclusive of `client.certificate_chain` since this value\nalso exists in that list.', - example: 'MII...', - default_field: false, - }, - { - name: 'client.certificate_chain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of PEM-encoded certificates that make up the certificate\nchain offered by the client. This is usually mutually-exclusive of `client.certificate`\nsince that value should be the first certificate in the chain.', - example: ['MII...', 'MII...'], - default_field: false, - }, - { - name: 'client.hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the MD5 digest of DER-encoded version\nof certificate offered by the client. For consistency with other hash values,\nthis value should be formatted as an uppercase hash.', - example: '0F76C7F2C55BFD7D8E8B8F4BFBF0C9EC', - default_field: false, - }, - { - name: 'client.hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the SHA1 digest of DER-encoded version\nof certificate offered by the client. For consistency with other hash values,\nthis value should be formatted as an uppercase hash.', - example: '9E393D93138888D288266C2D915214D1D1CCEB2A', - default_field: false, - }, - { - name: 'client.hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the SHA256 digest of DER-encoded\nversion of certificate offered by the client. For consistency with other hash\nvalues, this value should be formatted as an uppercase hash.', - example: '0687F666A054EF17A08E2F2162EAB4CBC0D265E1D7875BE74BF3C712CA92DAF0', - default_field: false, - }, - { - name: 'client.issuer', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Distinguished name of subject of the issuer of the x.509 certificate\npresented by the client.', - example: 'CN=MyDomain Root CA, OU=Infrastructure Team, DC=mydomain, DC=com', - default_field: false, - }, - { - name: 'client.ja3', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A hash that identifies clients based on how they perform an SSL/TLS\nhandshake.', - example: 'd4e5b18d6b55c71272893221c96ba240', - default_field: false, - }, - { - name: 'client.not_after', - level: 'extended', - type: 'date', - description: - 'Date/Time indicating when client certificate is no longer considered\nvalid.', - example: '2021-01-01T00:00:00.000Z', - default_field: false, - }, - { - name: 'client.not_before', - level: 'extended', - type: 'date', - description: 'Date/Time indicating when client certificate is first considered\nvalid.', - example: '1970-01-01T00:00:00.000Z', - default_field: false, - }, - { - name: 'client.server_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Also called an SNI, this tells the server which hostname to which\nthe client is attempting to connect. When this value is available, it should\nget copied to `destination.domain`.', - example: 'www.elastic.co', - default_field: false, - }, - { - name: 'client.subject', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Distinguished name of subject of the x.509 certificate presented\nby the client.', - example: 'CN=myclient, OU=Documentation Team, DC=mydomain, DC=com', - default_field: false, - }, - { - name: 'client.supported_ciphers', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Array of ciphers offered by the client during the client hello.', - example: [ - 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384', - 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384', - '...', - ], - default_field: false, - }, - { - name: 'curve', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'String indicating the curve used for the given cipher, when applicable.', - example: 'secp256r1', - default_field: false, - }, - { - name: 'established', - level: 'extended', - type: 'boolean', - description: - 'Boolean flag indicating if the TLS negotiation was successful and\ntransitioned to an encrypted tunnel.', - default_field: false, - }, - { - name: 'next_protocol', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'String indicating the protocol being tunneled. Per the values in\nthe IANA registry (https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids),\nthis string should be lower case.', - example: 'http/1.1', - default_field: false, - }, - { - name: 'resumed', - level: 'extended', - type: 'boolean', - description: - 'Boolean flag indicating if this TLS connection was resumed from\nan existing TLS negotiation.', - default_field: false, - }, - { - name: 'server.certificate', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'PEM-encoded stand-alone certificate offered by the server. This\nis usually mutually-exclusive of `server.certificate_chain` since this value\nalso exists in that list.', - example: 'MII...', - default_field: false, - }, - { - name: 'server.certificate_chain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Array of PEM-encoded certificates that make up the certificate\nchain offered by the server. This is usually mutually-exclusive of `server.certificate`\nsince that value should be the first certificate in the chain.', - example: ['MII...', 'MII...'], - default_field: false, - }, - { - name: 'server.hash.md5', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the MD5 digest of DER-encoded version\nof certificate offered by the server. For consistency with other hash values,\nthis value should be formatted as an uppercase hash.', - example: '0F76C7F2C55BFD7D8E8B8F4BFBF0C9EC', - default_field: false, - }, - { - name: 'server.hash.sha1', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the SHA1 digest of DER-encoded version\nof certificate offered by the server. For consistency with other hash values,\nthis value should be formatted as an uppercase hash.', - example: '9E393D93138888D288266C2D915214D1D1CCEB2A', - default_field: false, - }, - { - name: 'server.hash.sha256', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Certificate fingerprint using the SHA256 digest of DER-encoded\nversion of certificate offered by the server. For consistency with other hash\nvalues, this value should be formatted as an uppercase hash.', - example: '0687F666A054EF17A08E2F2162EAB4CBC0D265E1D7875BE74BF3C712CA92DAF0', - default_field: false, - }, - { - name: 'server.issuer', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Subject of the issuer of the x.509 certificate presented by the\nserver.', - example: 'CN=MyDomain Root CA, OU=Infrastructure Team, DC=mydomain, DC=com', - default_field: false, - }, - { - name: 'server.ja3s', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A hash that identifies servers based on how they perform an SSL/TLS\nhandshake.', - example: '394441ab65754e2207b1e1b457b3641d', - default_field: false, - }, - { - name: 'server.not_after', - level: 'extended', - type: 'date', - description: - 'Timestamp indicating when server certificate is no longer considered\nvalid.', - example: '2021-01-01T00:00:00.000Z', - default_field: false, - }, - { - name: 'server.not_before', - level: 'extended', - type: 'date', - description: 'Timestamp indicating when server certificate is first considered\nvalid.', - example: '1970-01-01T00:00:00.000Z', - default_field: false, - }, - { - name: 'server.subject', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Subject of the x.509 certificate presented by the server.', - example: 'CN=www.mydomain.com, OU=Infrastructure Team, DC=mydomain, DC=com', - default_field: false, - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Numeric part of the version parsed from the original string.', - example: '1.2', - default_field: false, - }, - { - name: 'version_protocol', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Normalized lowercase protocol name parsed from original string.', - example: 'tls', - default_field: false, - }, - ], - }, - { - name: 'tracing', - title: 'Tracing', - group: 2, - description: - 'Distributed tracing makes it possible to analyze performance throughout\na microservice architecture all in one view. This is accomplished by tracing\nall of the requests - from the initial web request in the front-end service\n- to queries made through multiple back-end services.', - type: 'group', - fields: [ - { - name: 'trace.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier of the trace.\n\nA trace groups multiple events like transactions that belong together. For\nexample, a user request handled by multiple inter-connected services.', - example: '4bf92f3577b34da6a3ce929d0e0e4736', - }, - { - name: 'transaction.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique identifier of the transaction.\n\nA transaction is the highest level of work measured within a service, such\nas a request to a server.', - example: '00f067aa0ba902b7', - }, - ], - }, - { - name: 'url', - title: 'URL', - group: 2, - description: - 'URL fields provide support for complete or partial URLs, and supports\nthe breaking down into scheme, domain, path, and so on.', - type: 'group', - fields: [ - { - name: 'domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Domain of the url, such as "www.elastic.co".\n\nIn some cases a URL may refer to an IP and/or port directly, without a domain\nname. In this case, the IP address would go to the `domain` field.', - example: 'www.elastic.co', - }, - { - name: 'extension', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The field contains the file extension from the original request\nurl.\n\nThe file extension is only set if it exists, as not every url has a file extension.\n\nThe leading period must not be included. For example, the value must be "png",\nnot ".png".', - example: 'png', - }, - { - name: 'fragment', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Portion of the url after the `#`, such as "top".\n\nThe `#` is not part of the fragment.', - }, - { - name: 'full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'If full URLs are important to your use case, they should be stored\nin `url.full`, whether this field is reconstructed or present in the event\nsource.', - example: 'https://www.elastic.co:443/search?q=elasticsearch#top', - }, - { - name: 'original', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: - 'Unmodified original url as seen in the event source.\n\nNote that in network monitoring, the observed URL may be a full URL, whereas\nin access logs, the URL is often just represented as a path.\n\nThis field is meant to represent the URL as it was observed, complete or not.', - example: - 'https://www.elastic.co:443/search?q=elasticsearch#top or /search?q=elasticsearch', - }, - { - name: 'password', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Password of the request.', - }, - { - name: 'path', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Path of the request, such as "/search".', - }, - { - name: 'port', - level: 'extended', - type: 'long', - format: 'string', - description: 'Port of the request, such as 443.', - example: 443, - }, - { - name: 'query', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The query field describes the query string of the request, such\nas "q=elasticsearch".\n\nThe `?` is excluded from the query string. If a URL contains no `?`, there\nis no query field. If there is a `?` but no query, the query field exists\nwith an empty string. The `exists` query can be used to differentiate between\nthe two cases.', - }, - { - name: 'registered_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The highest registered url domain, stripped of the subdomain.\n\nFor example, the registered domain for "foo.google.com" is "google.com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last two labels will not work well for TLDs such as "co.uk".', - example: 'google.com', - }, - { - name: 'scheme', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Scheme of the request, such as "https".\n\nNote: The `:` is not part of the scheme.', - example: 'https', - }, - { - name: 'top_level_domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The effective top level domain (eTLD), also known as the domain\nsuffix, is the last part of the domain name. For example, the top level domain\nfor google.com is "com".\n\nThis value can be determined precisely with a list like the public suffix\nlist (http://publicsuffix.org). Trying to approximate this by simply taking\nthe last label will not work well for effective TLDs such as "co.uk".', - example: 'co.uk', - }, - { - name: 'username', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Username of the request.', - }, - ], - }, - { - name: 'user', - title: 'User', - group: 2, - description: - 'The user fields describe information about the user that is relevant\nto the event.\n\nFields can have one entry or multiple entries. If a user has more than one id,\nprovide an array that includes all of them.', - type: 'group', - fields: [ - { - name: 'domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the user is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'email', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'User email address.', - }, - { - name: 'full_name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'group.domain', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Name of the directory the group is a member of.\n\nFor example, an LDAP or Active Directory domain name.', - }, - { - name: 'group.id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'group.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the group.', - }, - { - name: 'hash', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - ignore_above: 1024, - description: 'Unique identifiers of the user.', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'user_agent', - title: 'User agent', - group: 2, - description: - 'The user_agent fields normally come from a browser request.\n\nThey often show up in web service logs coming from the parsed user agent string.', - type: 'group', - fields: [ - { - name: 'device.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the device.', - example: 'iPhone', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Name of the user agent.', - example: 'Safari', - }, - { - name: 'original', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: 'Unparsed user_agent string.', - example: - 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_1 like Mac OS X) AppleWebKit/605.1.15\n(KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1', - }, - { - name: 'os.family', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'os.full', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'os.kernel', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'os.name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - default_field: false, - }, - ], - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'os.platform', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'os.version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Version of the user agent.', - example: 12, - }, - ], - }, - { - name: 'vlan', - title: 'VLAN', - group: 2, - description: - 'The VLAN fields are used to identify 802.1q tag(s) of a packet,\nas well as ingress and egress VLAN associations of an observer in relation to\na specific packet or connection.\n\nNetwork.vlan fields are used to record a single VLAN tag, or the outer tag in\nthe case of q-in-q encapsulations, for a packet or connection as observed, typically\nprovided by a network sensor (e.g. Zeek, Wireshark) passively reporting on traffic.\n\nNetwork.inner VLAN fields are used to report inner q-in-q 802.1q tags (multiple\n802.1q encapsulations) as observed, typically provided by a network sensor (e.g.\nZeek, Wireshark) passively reporting on traffic. Network.inner VLAN fields should\nonly be used in addition to network.vlan fields to indicate q-in-q tagging.\n\nObserver.ingress and observer.egress VLAN values are used to record observer\nspecific information when observer events contain discrete ingress and egress\nVLAN information, typically provided by firewalls, routers, or load balancers.', - type: 'group', - fields: [ - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'VLAN ID as reported by the observer.', - example: 10, - default_field: false, - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'Optional VLAN name as reported by the observer.', - example: 'outside', - default_field: false, - }, - ], - }, - { - name: 'vulnerability', - title: 'Vulnerability', - group: 2, - description: - 'The vulnerability fields describe information about a vulnerability\nthat is relevant to an event.', - type: 'group', - fields: [ - { - name: 'category', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of system or architecture that the vulnerability affects.\nThese may be platform-specific (for example, Debian or SUSE) or general (for\nexample, Database or Firewall). For example (https://qualysguard.qualys.com/qwebhelp/fo_portal/knowledgebase/vulnerability_categories.htm[Qualys\nvulnerability categories])\n\nThis field must be an array.', - example: '["Firewall"]', - default_field: false, - }, - { - name: 'classification', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The classification of the vulnerability scoring system. For example\n(https://www.first.org/cvss/)', - example: 'CVSS', - default_field: false, - }, - { - name: 'description', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - multi_fields: [ - { - name: 'text', - type: 'text', - norms: false, - }, - ], - description: - 'The description of the vulnerability that provides additional context\nof the vulnerability. For example (https://cve.mitre.org/about/faqs.html#cve_entry_descriptions_created[Common\nVulnerabilities and Exposure CVE description])', - example: 'In macOS before 2.12.6, there is a vulnerability in the RPC...', - default_field: false, - }, - { - name: 'enumeration', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The type of identifier used for this vulnerability. For example\n(https://cve.mitre.org/about/)', - example: 'CVE', - default_field: false, - }, - { - name: 'id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The identification (ID) is the number portion of a vulnerability\nentry. It includes a unique identification number for the vulnerability. For\nexample (https://cve.mitre.org/about/faqs.html#what_is_cve_id)[Common Vulnerabilities\nand Exposure CVE ID]', - example: 'CVE-2019-00001', - default_field: false, - }, - { - name: 'reference', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'A resource that provides additional information, context, and mitigations\nfor the identified vulnerability.', - example: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-6111', - default_field: false, - }, - { - name: 'report_id', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The report or scan identification number.', - example: 20191018.0001, - default_field: false, - }, - { - name: 'scanner.vendor', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: 'The name of the vulnerability scanner vendor.', - example: 'Tenable', - default_field: false, - }, - { - name: 'score.base', - level: 'extended', - type: 'float', - description: - 'Scores can range from 0.0 to 10.0, with 10.0 being the most severe.\n\nBase scores cover an assessment for exploitability metrics (attack vector,\ncomplexity, privileges, and user interaction), impact metrics (confidentiality,\nintegrity, and availability), and scope. For example (https://www.first.org/cvss/specification-document)', - example: 5.5, - default_field: false, - }, - { - name: 'score.environmental', - level: 'extended', - type: 'float', - description: - 'Scores can range from 0.0 to 10.0, with 10.0 being the most severe.\n\nEnvironmental scores cover an assessment for any modified Base metrics, confidentiality,\nintegrity, and availability requirements. For example (https://www.first.org/cvss/specification-document)', - example: 5.5, - default_field: false, - }, - { - name: 'score.temporal', - level: 'extended', - type: 'float', - description: - 'Scores can range from 0.0 to 10.0, with 10.0 being the most severe.\n\nTemporal scores cover an assessment for code maturity, remediation level,\nand confidence. For example (https://www.first.org/cvss/specification-document)', - default_field: false, - }, - { - name: 'score.version', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The National Vulnerability Database (NVD) provides qualitative\nseverity rankings of "Low", "Medium", and "High" for CVSS v2.0 base score\nranges in addition to the severity ratings for CVSS v3.0 as they are defined\nin the CVSS v3.0 specification.\n\nCVSS is owned and managed by FIRST.Org, Inc. (FIRST), a US-based non-profit\norganization, whose mission is to help computer security incident response\nteams across the world. For example (https://nvd.nist.gov/vuln-metrics/cvss)', - example: 2, - default_field: false, - }, - { - name: 'severity', - level: 'extended', - type: 'keyword', - ignore_above: 1024, - description: - 'The severity of the vulnerability can help with metrics and internal\nprioritization regarding remediation. For example (https://nvd.nist.gov/vuln-metrics/cvss)', - example: 'Critical', - default_field: false, - }, - ], - }, - ], - }, - { - key: 'beat', - anchor: 'beat-common', - title: 'Beat', - description: 'Contains common beat fields available in all event types.\n', - fields: [ - { - name: 'agent.hostname', - type: 'keyword', - description: 'Hostname of the agent.', - }, - { - name: 'beat.timezone', - type: 'alias', - path: 'event.timezone', - migration: true, - }, - { - name: 'fields', - type: 'object', - object_type: 'keyword', - description: 'Contains user configurable fields.\n', - }, - { - name: 'beat.name', - type: 'alias', - path: 'host.name', - migration: true, - }, - { - name: 'beat.hostname', - type: 'alias', - path: 'agent.hostname', - migration: true, - }, - { - name: 'timeseries.instance', - type: 'keyword', - description: 'Time series instance id', - }, - ], - }, - { - key: 'cloud', - title: 'Cloud provider metadata', - description: 'Metadata from cloud providers added by the add_cloud_metadata processor.\n', - fields: [ - { - name: 'cloud.project.id', - example: 'project-x', - description: 'Name of the project in Google Cloud.\n', - }, - { - name: 'cloud.image.id', - example: 'ami-abcd1234', - description: 'Image ID for the cloud instance.\n', - }, - { - name: 'meta.cloud.provider', - type: 'alias', - path: 'cloud.provider', - migration: true, - }, - { - name: 'meta.cloud.instance_id', - type: 'alias', - path: 'cloud.instance.id', - migration: true, - }, - { - name: 'meta.cloud.instance_name', - type: 'alias', - path: 'cloud.instance.name', - migration: true, - }, - { - name: 'meta.cloud.machine_type', - type: 'alias', - path: 'cloud.machine.type', - migration: true, - }, - { - name: 'meta.cloud.availability_zone', - type: 'alias', - path: 'cloud.availability_zone', - migration: true, - }, - { - name: 'meta.cloud.project_id', - type: 'alias', - path: 'cloud.project.id', - migration: true, - }, - { - name: 'meta.cloud.region', - type: 'alias', - path: 'cloud.region', - migration: true, - }, - ], - }, - { - key: 'docker', - title: 'Docker', - description: 'Docker stats collected from Docker.\n', - short_config: false, - anchor: 'docker-processor', - fields: [ - { - name: 'docker', - type: 'group', - fields: [ - { - name: 'container.id', - type: 'alias', - path: 'container.id', - migration: true, - }, - { - name: 'container.image', - type: 'alias', - path: 'container.image.name', - migration: true, - }, - { - name: 'container.name', - type: 'alias', - path: 'container.name', - migration: true, - }, - { - name: 'container.labels', - type: 'object', - object_type: 'keyword', - description: 'Image labels.\n', - }, - ], - }, - ], - }, - { - key: 'host', - title: 'Host', - description: 'Info collected for the host machine.\n', - anchor: 'host-processor', - fields: [ - { - name: 'host', - type: 'group', - fields: [ - { - name: 'containerized', - type: 'boolean', - description: 'If the host is a container.\n', - }, - { - name: 'os.build', - type: 'keyword', - example: '18D109', - description: 'OS build information.\n', - }, - { - name: 'os.codename', - type: 'keyword', - example: 'stretch', - description: 'OS codename, if any.\n', - }, - ], - }, - ], - }, - { - key: 'kubernetes', - title: 'Kubernetes', - description: 'Kubernetes metadata added by the kubernetes processor\n', - short_config: false, - anchor: 'kubernetes-processor', - fields: [ - { - name: 'kubernetes', - type: 'group', - fields: [ - { - name: 'pod.name', - type: 'keyword', - description: 'Kubernetes pod name\n', - }, - { - name: 'pod.uid', - type: 'keyword', - description: 'Kubernetes Pod UID\n', - }, - { - name: 'namespace', - type: 'keyword', - description: 'Kubernetes namespace\n', - }, - { - name: 'node.name', - type: 'keyword', - description: 'Kubernetes node name\n', - }, - { - name: 'labels.*', - type: 'object', - object_type: 'keyword', - object_type_mapping_type: '*', - description: 'Kubernetes labels map\n', - }, - { - name: 'annotations.*', - type: 'object', - object_type: 'keyword', - object_type_mapping_type: '*', - description: 'Kubernetes annotations map\n', - }, - { - name: 'replicaset.name', - type: 'keyword', - description: 'Kubernetes replicaset name\n', - }, - { - name: 'deployment.name', - type: 'keyword', - description: 'Kubernetes deployment name\n', - }, - { - name: 'statefulset.name', - type: 'keyword', - description: 'Kubernetes statefulset name\n', - }, - { - name: 'container.name', - type: 'keyword', - description: 'Kubernetes container name\n', - }, - { - name: 'container.image', - type: 'keyword', - description: 'Kubernetes container image\n', - }, - ], - }, - ], - }, - { - key: 'process', - title: 'Process', - description: 'Process metadata fields\n', - fields: [ - { - name: 'process', - type: 'group', - fields: [ - { - name: 'exe', - type: 'alias', - path: 'process.executable', - migration: true, - }, - ], - }, - ], - }, - { - key: 'jolokia-autodiscover', - title: 'Jolokia Discovery autodiscover provider', - description: 'Metadata from Jolokia Discovery added by the jolokia provider.\n', - fields: [ - { - name: 'jolokia.agent.version', - type: 'keyword', - description: 'Version number of jolokia agent.\n', - }, - { - name: 'jolokia.agent.id', - type: 'keyword', - description: - 'Each agent has a unique id which can be either provided during startup of the agent in form of a configuration parameter or being autodetected. If autodected, the id has several parts: The IP, the process id, hashcode of the agent and its type.\n', - }, - { - name: 'jolokia.server.product', - type: 'keyword', - description: 'The container product if detected.\n', - }, - { - name: 'jolokia.server.version', - type: 'keyword', - description: "The container's version (if detected).\n", - }, - { - name: 'jolokia.server.vendor', - type: 'keyword', - description: 'The vendor of the container the agent is running in.\n', - }, - { - name: 'jolokia.url', - type: 'keyword', - description: 'The URL how this agent can be contacted.\n', - }, - { - name: 'jolokia.secured', - type: 'boolean', - description: 'Whether the agent was configured for authentication or not.\n', - }, - ], - }, - { - key: 'common', - title: 'Common', - description: - 'These fields contain data about the environment in which the transaction or flow was captured.\n', - fields: [ - { - name: 'type', - description: - 'The type of the transaction (for example, HTTP, MySQL, Redis, or RUM) or "flow" in case of flows.\n', - required: true, - }, - { - name: 'server.process.name', - description: 'The name of the process that served the transaction.\n', - }, - { - name: 'server.process.args', - description: 'The command-line of the process that served the transaction.\n', - }, - { - name: 'server.process.executable', - description: 'Absolute path to the server process executable.\n', - }, - { - name: 'server.process.working_directory', - description: 'The working directory of the server process.\n', - }, - { - name: 'server.process.start', - description: 'The time the server process started.\n', - }, - { - name: 'client.process.name', - description: 'The name of the process that initiated the transaction.\n', - }, - { - name: 'client.process.args', - description: 'The command-line of the process that initiated the transaction.\n', - }, - { - name: 'client.process.executable', - description: 'Absolute path to the client process executable.\n', - }, - { - name: 'client.process.working_directory', - description: 'The working directory of the client process.\n', - }, - { - name: 'client.process.start', - description: 'The time the client process started.\n', - }, - { - name: 'real_ip', - type: 'alias', - path: 'network.forwarded_ip', - migration: true, - description: - 'If the server initiating the transaction is a proxy, this field contains the original client IP address. For HTTP, for example, the IP address extracted from a configurable HTTP header, by default `X-Forwarded-For`.\nUnless this field is disabled, it always has a value, and it matches the `client_ip` for non proxy clients.\n', - }, - { - name: 'transport', - type: 'alias', - path: 'network.transport', - migration: true, - description: - 'The transport protocol used for the transaction. If not specified, then tcp is assumed.\n', - }, - ], - }, - { - key: 'flows_event', - title: 'Flow Event', - description: 'These fields contain data about the flow itself.\n', - fields: [ - { - name: 'flow.final', - type: 'boolean', - description: - 'Indicates if event is last event in flow. If final is false, the event reports an intermediate flow state only.\n', - }, - { - name: 'flow.id', - description: 'Internal flow ID based on connection meta data and address.\n', - }, - { - name: 'flow.vlan', - type: 'long', - description: - "VLAN identifier from the 802.1q frame. In case of a multi-tagged frame this field will be an array with the outer tag's VLAN identifier listed first.\n", - }, - { - name: 'flow_id', - type: 'alias', - path: 'flow.id', - migration: true, - }, - { - name: 'final', - type: 'alias', - path: 'flow.final', - migration: true, - }, - { - name: 'vlan', - type: 'alias', - path: 'flow.vlan', - migration: true, - }, - { - name: 'source.stats.net_bytes_total', - type: 'alias', - path: 'source.bytes', - migration: true, - }, - { - name: 'source.stats.net_packets_total', - type: 'alias', - path: 'source.packets', - migration: true, - }, - { - name: 'dest.stats.net_bytes_total', - type: 'alias', - path: 'destination.bytes', - migration: true, - }, - { - name: 'dest.stats.net_packets_total', - type: 'alias', - path: 'destination.packets', - migration: true, - }, - ], - }, - { - key: 'trans_event', - title: 'Transaction Event', - description: 'These fields contain data about the transaction itself.\n', - fields: [ - { - name: 'status', - description: - 'The high level status of the transaction. The way to compute this value depends on the protocol, but the result has a meaning independent of the protocol.\n', - required: true, - possible_values: ['OK', 'Error', 'Server Error', 'Client Error'], - }, - { - name: 'method', - description: - 'The command/verb/method of the transaction. For HTTP, this is the method name (GET, POST, PUT, and so on), for SQL this is the verb (SELECT, UPDATE, DELETE, and so on).\n', - }, - { - name: 'resource', - description: - 'The logical resource that this transaction refers to. For HTTP, this is the URL path up to the last slash (/). For example, if the URL is `/users/1`, the resource is `/users`. For databases, the resource is typically the table name. The field is not filled for all transaction types.\n', - }, - { - name: 'path', - required: true, - description: - 'The path the transaction refers to. For HTTP, this is the URL. For SQL databases, this is the table name. For key-value stores, this is the key.\n', - }, - { - name: 'query', - type: 'keyword', - description: - 'The query in a human readable format. For HTTP, it will typically be something like `GET /users/_search?name=test`. For MySQL, it is something like `SELECT id from users where name=test`.\n', - }, - { - name: 'params', - type: 'text', - description: - 'The request parameters. For HTTP, these are the POST or GET parameters. For Thrift-RPC, these are the parameters from the request.\n', - }, - { - name: 'notes', - type: 'alias', - path: 'error.message', - description: - 'Messages from Packetbeat itself. This field usually contains error messages for interpreting the raw data. This information can be helpful for troubleshooting.\n', - }, - ], - }, - { - key: 'raw', - title: 'Raw', - description: 'These fields contain the raw transaction data.', - fields: [ - { - name: 'request', - type: 'text', - description: - 'For text protocols, this is the request as seen on the wire (application layer only). For binary protocols this is our representation of the request.\n', - }, - { - name: 'response', - type: 'text', - description: - 'For text protocols, this is the response as seen on the wire (application layer only). For binary protocols this is our representation of the request.\n', - }, - ], - }, - { - key: 'trans_measurements', - title: 'Measurements (Transactions)', - description: 'These fields contain measurements related to the transaction.\n', - fields: [ - { - name: 'bytes_in', - type: 'alias', - path: 'source.bytes', - description: - 'The number of bytes of the request. Note that this size is the application layer message length, without the length of the IP or TCP headers.\n', - }, - { - name: 'bytes_out', - type: 'alias', - path: 'destination.bytes', - description: - 'The number of bytes of the response. Note that this size is the application layer message length, without the length of the IP or TCP headers.\n', - }, - ], - }, - { - key: 'amqp', - title: 'AMQP', - description: 'AMQP specific event fields.', - fields: [ - { - name: 'amqp', - type: 'group', - fields: [ - { - name: 'reply-code', - type: 'long', - description: 'AMQP reply code to an error, similar to http reply-code\n', - example: 404, - }, - { - name: 'reply-text', - type: 'keyword', - description: 'Text explaining the error.\n', - }, - { - name: 'class-id', - type: 'long', - description: 'Failing method class.\n', - }, - { - name: 'method-id', - type: 'long', - description: 'Failing method ID.\n', - }, - { - name: 'exchange', - type: 'keyword', - description: 'Name of the exchange.\n', - }, - { - name: 'exchange-type', - type: 'keyword', - description: 'Exchange type.\n', - example: 'fanout', - }, - { - name: 'passive', - type: 'boolean', - description: 'If set, do not create exchange/queue.\n', - }, - { - name: 'durable', - type: 'boolean', - description: 'If set, request a durable exchange/queue.\n', - }, - { - name: 'exclusive', - type: 'boolean', - description: 'If set, request an exclusive queue.\n', - }, - { - name: 'auto-delete', - type: 'boolean', - description: 'If set, auto-delete queue when unused.\n', - }, - { - name: 'no-wait', - type: 'boolean', - description: 'If set, the server will not respond to the method.\n', - }, - { - name: 'consumer-tag', - description: 'Identifier for the consumer, valid within the current channel.\n', - }, - { - name: 'delivery-tag', - type: 'long', - description: 'The server-assigned and channel-specific delivery tag.\n', - }, - { - name: 'message-count', - type: 'long', - description: - 'The number of messages in the queue, which will be zero for newly-declared queues.\n', - }, - { - name: 'consumer-count', - type: 'long', - description: 'The number of consumers of a queue.\n', - }, - { - name: 'routing-key', - type: 'keyword', - description: 'Message routing key.\n', - }, - { - name: 'no-ack', - type: 'boolean', - description: 'If set, the server does not expect acknowledgements for messages.\n', - }, - { - name: 'no-local', - type: 'boolean', - description: - 'If set, the server will not send messages to the connection that published them.\n', - }, - { - name: 'if-unused', - type: 'boolean', - description: 'Delete only if unused.\n', - }, - { - name: 'if-empty', - type: 'boolean', - description: 'Delete only if empty.\n', - }, - { - name: 'queue', - type: 'keyword', - description: 'The queue name identifies the queue within the vhost.\n', - }, - { - name: 'redelivered', - type: 'boolean', - description: - 'Indicates that the message has been previously delivered to this or another client.\n', - }, - { - name: 'multiple', - type: 'boolean', - description: 'Acknowledge multiple messages.\n', - }, - { - name: 'arguments', - type: 'object', - description: - 'Optional additional arguments passed to some methods. Can be of various types.\n', - }, - { - name: 'mandatory', - type: 'boolean', - description: 'Indicates mandatory routing.\n', - }, - { - name: 'immediate', - type: 'boolean', - description: 'Request immediate delivery.\n', - }, - { - name: 'content-type', - type: 'keyword', - description: 'MIME content type.\n', - example: 'text/plain', - }, - { - name: 'content-encoding', - type: 'keyword', - description: 'MIME content encoding.\n', - }, - { - name: 'headers', - type: 'object', - object_type: 'keyword', - description: 'Message header field table.\n', - }, - { - name: 'delivery-mode', - type: 'keyword', - description: 'Non-persistent (1) or persistent (2).\n', - }, - { - name: 'priority', - type: 'long', - description: 'Message priority, 0 to 9.\n', - }, - { - name: 'correlation-id', - type: 'keyword', - description: 'Application correlation identifier.\n', - }, - { - name: 'reply-to', - type: 'keyword', - description: 'Address to reply to.\n', - }, - { - name: 'expiration', - type: 'keyword', - description: 'Message expiration specification.\n', - }, - { - name: 'message-id', - type: 'keyword', - description: 'Application message identifier.\n', - }, - { - name: 'timestamp', - type: 'keyword', - description: 'Message timestamp.\n', - }, - { - name: 'type', - type: 'keyword', - description: 'Message type name.\n', - }, - { - name: 'user-id', - type: 'keyword', - description: 'Creating user id.\n', - }, - { - name: 'app-id', - type: 'keyword', - description: 'Creating application id.\n', - }, - ], - }, - ], - }, - { - key: 'cassandra', - title: 'Cassandra', - description: 'Cassandra v4/3 specific event fields.', - fields: [ - { - name: 'no_request', - type: 'alias', - path: 'cassandra.no_request', - migration: true, - }, - { - name: 'cassandra', - type: 'group', - description: 'Information about the Cassandra request and response.', - fields: [ - { - name: 'no_request', - type: 'boolean', - description: 'Indicates that there is no request because this is a PUSH message.\n', - }, - { - name: 'request', - type: 'group', - description: 'Cassandra request.', - fields: [ - { - name: 'headers', - type: 'group', - description: 'Cassandra request headers.', - fields: [ - { - name: 'version', - type: 'long', - description: 'The version of the protocol.', - }, - { - name: 'flags', - type: 'keyword', - description: 'Flags applying to this frame.', - }, - { - name: 'stream', - type: 'keyword', - description: - 'A frame has a stream id. If a client sends a request message with the stream id X, it is guaranteed that the stream id of the response to that message will be X.', - }, - { - name: 'op', - type: 'keyword', - description: 'An operation type that distinguishes the actual message.', - }, - { - name: 'length', - type: 'long', - description: - 'A integer representing the length of the body of the frame (a frame is limited to 256MB in length).', - }, - ], - }, - { - name: 'query', - type: 'keyword', - description: 'The CQL query which client send to cassandra.', - }, - ], - }, - { - name: 'response', - type: 'group', - description: 'Cassandra response.', - fields: [ - { - name: 'headers', - type: 'group', - description: - "Cassandra response headers, the structure is as same as request's header.", - fields: [ - { - name: 'version', - type: 'long', - description: 'The version of the protocol.', - }, - { - name: 'flags', - type: 'keyword', - description: 'Flags applying to this frame.', - }, - { - name: 'stream', - type: 'keyword', - description: - 'A frame has a stream id. If a client sends a request message with the stream id X, it is guaranteed that the stream id of the response to that message will be X.', - }, - { - name: 'op', - type: 'keyword', - description: 'An operation type that distinguishes the actual message.', - }, - { - name: 'length', - type: 'long', - description: - 'A integer representing the length of the body of the frame (a frame is limited to 256MB in length).', - }, - ], - }, - { - name: 'result', - type: 'group', - description: 'Details about the returned result.', - fields: [ - { - name: 'type', - type: 'keyword', - description: 'Cassandra result type.', - }, - { - name: 'rows', - type: 'group', - description: 'Details about the rows.', - fields: [ - { - name: 'num_rows', - type: 'long', - description: 'Representing the number of rows present in this result.', - }, - { - name: 'meta', - type: 'group', - description: 'Composed of result metadata.', - fields: [ - { - name: 'keyspace', - type: 'keyword', - description: - 'Only present after set Global_tables_spec, the keyspace name.', - }, - { - name: 'table', - type: 'keyword', - description: - 'Only present after set Global_tables_spec, the table name.', - }, - { - name: 'flags', - type: 'keyword', - description: - 'Provides information on the formatting of the remaining information.', - }, - { - name: 'col_count', - type: 'long', - description: - 'Representing the number of columns selected by the query that produced this result.', - }, - { - name: 'pkey_columns', - type: 'long', - description: 'Representing the PK columns index and counts.', - }, - { - name: 'paging_state', - type: 'keyword', - description: - 'The paging_state is a bytes value that should be used in QUERY/EXECUTE to continue paging and retrieve the remainder of the result for this query.', - }, - ], - }, - ], - }, - { - name: 'keyspace', - type: 'keyword', - description: 'Indicating the name of the keyspace that has been set.', - }, - { - name: 'schema_change', - type: 'group', - description: 'The result to a schema_change message.', - fields: [ - { - name: 'change', - type: 'keyword', - description: 'Representing the type of changed involved.', - }, - { - name: 'keyspace', - type: 'keyword', - description: 'This describes which keyspace has changed.', - }, - { - name: 'table', - type: 'keyword', - description: 'This describes which table has changed.', - }, - { - name: 'object', - type: 'keyword', - description: - 'This describes the name of said affected object (either the table, user type, function, or aggregate name).', - }, - { - name: 'target', - type: 'keyword', - description: - 'Target could be "FUNCTION" or "AGGREGATE", multiple arguments.', - }, - { - name: 'name', - type: 'keyword', - description: 'The function/aggregate name.', - }, - { - name: 'args', - type: 'keyword', - description: 'One string for each argument type (as CQL type).', - }, - ], - }, - { - name: 'prepared', - type: 'group', - description: 'The result to a PREPARE message.', - fields: [ - { - name: 'prepared_id', - type: 'keyword', - description: 'Representing the prepared query ID.', - }, - { - name: 'req_meta', - type: 'group', - description: 'This describes the request metadata.', - fields: [ - { - name: 'keyspace', - type: 'keyword', - description: - 'Only present after set Global_tables_spec, the keyspace name.', - }, - { - name: 'table', - type: 'keyword', - description: - 'Only present after set Global_tables_spec, the table name.', - }, - { - name: 'flags', - type: 'keyword', - description: - 'Provides information on the formatting of the remaining information.', - }, - { - name: 'col_count', - type: 'long', - description: - 'Representing the number of columns selected by the query that produced this result.', - }, - { - name: 'pkey_columns', - type: 'long', - description: 'Representing the PK columns index and counts.', - }, - { - name: 'paging_state', - type: 'keyword', - description: - 'The paging_state is a bytes value that should be used in QUERY/EXECUTE to continue paging and retrieve the remainder of the result for this query.', - }, - ], - }, - { - name: 'resp_meta', - type: 'group', - description: 'This describes the metadata for the result set.', - fields: [ - { - name: 'keyspace', - type: 'keyword', - description: - 'Only present after set Global_tables_spec, the keyspace name.', - }, - { - name: 'table', - type: 'keyword', - description: - 'Only present after set Global_tables_spec, the table name.', - }, - { - name: 'flags', - type: 'keyword', - description: - 'Provides information on the formatting of the remaining information.', - }, - { - name: 'col_count', - type: 'long', - description: - 'Representing the number of columns selected by the query that produced this result.', - }, - { - name: 'pkey_columns', - type: 'long', - description: 'Representing the PK columns index and counts.', - }, - { - name: 'paging_state', - type: 'keyword', - description: - 'The paging_state is a bytes value that should be used in QUERY/EXECUTE to continue paging and retrieve the remainder of the result for this query.', - }, - ], - }, - ], - }, - ], - }, - { - name: 'supported', - type: 'object', - object_type: 'keyword', - description: - 'Indicates which startup options are supported by the server. This message comes as a response to an OPTIONS message.', - }, - { - name: 'authentication', - type: 'group', - description: - 'Indicates that the server requires authentication, and which authentication mechanism to use.', - fields: [ - { - name: 'class', - type: 'keyword', - description: 'Indicates the full class name of the IAuthenticator in use', - }, - ], - }, - { - name: 'warnings', - type: 'keyword', - description: 'The text of the warnings, only occur when Warning flag was set.', - }, - { - name: 'event', - type: 'group', - description: - 'Event pushed by the server. A client will only receive events for the types it has REGISTERed to.', - fields: [ - { - name: 'type', - type: 'keyword', - description: 'Representing the event type.', - }, - { - name: 'change', - type: 'keyword', - description: - 'The message corresponding respectively to the type of change followed by the address of the new/removed node.', - }, - { - name: 'host', - type: 'keyword', - description: 'Representing the node ip.', - }, - { - name: 'port', - type: 'long', - description: 'Representing the node port.', - }, - { - name: 'schema_change', - type: 'group', - description: 'The events details related to schema change.', - fields: [ - { - name: 'change', - type: 'keyword', - description: 'Representing the type of changed involved.', - }, - { - name: 'keyspace', - type: 'keyword', - description: 'This describes which keyspace has changed.', - }, - { - name: 'table', - type: 'keyword', - description: 'This describes which table has changed.', - }, - { - name: 'object', - type: 'keyword', - description: - 'This describes the name of said affected object (either the table, user type, function, or aggregate name).', - }, - { - name: 'target', - type: 'keyword', - description: - 'Target could be "FUNCTION" or "AGGREGATE", multiple arguments.', - }, - { - name: 'name', - type: 'keyword', - description: 'The function/aggregate name.', - }, - { - name: 'args', - type: 'keyword', - description: 'One string for each argument type (as CQL type).', - }, - ], - }, - ], - }, - { - name: 'error', - type: 'group', - description: - 'Indicates an error processing a request. The body of the message will be an error code followed by a error message. Then, depending on the exception, more content may follow.', - fields: [ - { - name: 'code', - type: 'long', - description: 'The error code of the Cassandra response.', - }, - { - name: 'msg', - type: 'keyword', - description: 'The error message of the Cassandra response.', - }, - { - name: 'type', - type: 'keyword', - description: 'The error type of the Cassandra response.', - }, - { - name: 'details', - type: 'group', - description: 'The details of the error.', - fields: [ - { - name: 'read_consistency', - type: 'keyword', - description: - 'Representing the consistency level of the query that triggered the exception.', - }, - { - name: 'required', - type: 'long', - description: - 'Representing the number of nodes that should be alive to respect consistency level.', - }, - { - name: 'alive', - type: 'long', - description: - 'Representing the number of replicas that were known to be alive when the request had been processed (since an unavailable exception has been triggered).', - }, - { - name: 'received', - type: 'long', - description: - 'Representing the number of nodes having acknowledged the request.', - }, - { - name: 'blockfor', - type: 'long', - description: - 'Representing the number of replicas whose acknowledgement is required to achieve consistency level.', - }, - { - name: 'write_type', - type: 'keyword', - description: 'Describe the type of the write that timed out.', - }, - { - name: 'data_present', - type: 'boolean', - description: 'It means the replica that was asked for data had responded.', - }, - { - name: 'keyspace', - type: 'keyword', - description: 'The keyspace of the failed function.', - }, - { - name: 'table', - type: 'keyword', - description: 'The keyspace of the failed function.', - }, - { - name: 'stmt_id', - type: 'keyword', - description: 'Representing the unknown ID.', - }, - { - name: 'num_failures', - type: 'keyword', - description: - 'Representing the number of nodes that experience a failure while executing the request.', - }, - { - name: 'function', - type: 'keyword', - description: 'The name of the failed function.', - }, - { - name: 'arg_types', - type: 'keyword', - description: - 'One string for each argument type (as CQL type) of the failed function.', - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], - }, - { - key: 'dhcpv4', - title: 'DHCPv4', - description: 'DHCPv4 event fields', - fields: [ - { - name: 'dhcpv4', - type: 'group', - fields: [ - { - name: 'transaction_id', - type: 'keyword', - description: - 'Transaction ID, a random number chosen by the\nclient, used by the client and server to associate\nmessages and responses between a client and a\nserver.\n', - }, - { - name: 'seconds', - type: 'long', - description: - 'Number of seconds elapsed since client began address acquisition or\nrenewal process.\n', - }, - { - name: 'flags', - type: 'keyword', - description: - 'Flags are set by the client to indicate how the DHCP server should\nits reply -- either unicast or broadcast.\n', - }, - { - name: 'client_ip', - type: 'ip', - description: 'The current IP address of the client.', - }, - { - name: 'assigned_ip', - type: 'ip', - description: - 'The IP address that the DHCP server is assigning to the client.\nThis field is also known as "your" IP address.\n', - }, - { - name: 'server_ip', - type: 'ip', - description: - 'The IP address of the DHCP server that the client should use for the\nnext step in the bootstrap process.\n', - }, - { - name: 'relay_ip', - type: 'ip', - description: - 'The relay IP address used by the client to contact the server\n(i.e. a DHCP relay server).\n', - }, - { - name: 'client_mac', - type: 'keyword', - description: "The client's MAC address (layer two).", - }, - { - name: 'server_name', - type: 'keyword', - description: - 'The name of the server sending the message. Optional. Used in\nDHCPOFFER or DHCPACK messages.\n', - }, - { - name: 'op_code', - type: 'keyword', - example: 'bootreply', - description: 'The message op code (bootrequest or bootreply).\n', - }, - { - name: 'hops', - type: 'long', - description: 'The number of hops the DHCP message went through.', - }, - { - name: 'hardware_type', - type: 'keyword', - description: - 'The type of hardware used for the local network (Ethernet,\nLocalTalk, etc).\n', - }, - { - name: 'option', - type: 'group', - fields: [ - { - name: 'message_type', - type: 'keyword', - example: 'ack', - description: - 'The specific type of DHCP message being sent (e.g. discover,\noffer, request, decline, ack, nak, release, inform).\n', - }, - { - name: 'parameter_request_list', - type: 'keyword', - description: - 'This option is used by a DHCP client to request values for\nspecified configuration parameters.\n', - }, - { - name: 'requested_ip_address', - type: 'ip', - description: - 'This option is used in a client request (DHCPDISCOVER) to allow\nthe client to request that a particular IP address be assigned.\n', - }, - { - name: 'server_identifier', - type: 'ip', - description: - 'IP address of the individual DHCP server which handled this\nmessage.\n', - }, - { - name: 'broadcast_address', - type: 'ip', - description: - "This option specifies the broadcast address in use on the\nclient's subnet.\n", - }, - { - name: 'max_dhcp_message_size', - type: 'long', - description: - 'This option specifies the maximum length DHCP message that the\nclient is willing to accept.\n', - }, - { - name: 'class_identifier', - type: 'keyword', - description: - "This option is used by DHCP clients to optionally identify the\nvendor type and configuration of a DHCP client. Vendors may\nchoose to define specific vendor class identifiers to convey\nparticular configuration or other identification information\nabout a client. For example, the identifier may encode the\nclient's hardware configuration.\n", - }, - { - name: 'domain_name', - type: 'keyword', - description: - 'This option specifies the domain name that client should use\nwhen resolving hostnames via the Domain Name System.\n', - }, - { - name: 'dns_servers', - type: 'ip', - description: - 'The domain name server option specifies a list of Domain Name\nSystem servers available to the client.\n', - }, - { - name: 'vendor_identifying_options', - type: 'object', - description: - 'A DHCP client may use this option to unambiguously identify the\nvendor that manufactured the hardware on which the client is\nrunning, the software in use, or an industry consortium to which\nthe vendor belongs. This field is described in RFC 3925.\n', - }, - { - name: 'subnet_mask', - type: 'ip', - description: - 'The subnet mask that the client should use on the currnet\nnetwork.\n', - }, - { - name: 'utc_time_offset_sec', - type: 'long', - description: - "The time offset field specifies the offset of the client's\nsubnet in seconds from Coordinated Universal Time (UTC).\n", - }, - { - name: 'router', - type: 'ip', - description: - "The router option specifies a list of IP addresses for routers\non the client's subnet.\n", - }, - { - name: 'time_servers', - type: 'ip', - description: - 'The time server option specifies a list of RFC 868 time servers\navailable to the client.\n', - }, - { - name: 'ntp_servers', - type: 'ip', - description: - 'This option specifies a list of IP addresses indicating NTP\nservers available to the client.\n', - }, - { - name: 'hostname', - type: 'keyword', - description: 'This option specifies the name of the client.\n', - }, - { - name: 'ip_address_lease_time_sec', - type: 'long', - description: - 'This option is used in a client request (DHCPDISCOVER or\nDHCPREQUEST) to allow the client to request a lease time for the\nIP address. In a server reply (DHCPOFFER), a DHCP server uses\nthis option to specify the lease time it is willing to offer.\n', - }, - { - name: 'message', - type: 'text', - description: - 'This option is used by a DHCP server to provide an error message\nto a DHCP client in a DHCPNAK message in the event of a failure.\nA client may use this option in a DHCPDECLINE message to\nindicate the why the client declined the offered parameters.\n', - }, - { - name: 'renewal_time_sec', - type: 'long', - description: - 'This option specifies the time interval from address assignment\nuntil the client transitions to the RENEWING state.\n', - }, - { - name: 'rebinding_time_sec', - type: 'long', - description: - 'This option specifies the time interval from address assignment\nuntil the client transitions to the REBINDING state.\n', - }, - { - name: 'boot_file_name', - type: 'keyword', - description: - "This option is used to identify a bootfile when the 'file' field\nin the DHCP header has been used for DHCP options.\n", - }, - ], - }, - ], - }, - ], - }, - { - key: 'dns', - title: 'DNS', - description: 'DNS-specific event fields.', - fields: [ - { - name: 'dns', - type: 'group', - fields: [ - { - name: 'flags.authoritative', - type: 'boolean', - description: - 'A DNS flag specifying that the responding server is an authority for the domain name used in the question.\n', - }, - { - name: 'flags.recursion_available', - type: 'boolean', - description: - 'A DNS flag specifying whether recursive query support is available in the name server.\n', - }, - { - name: 'flags.recursion_desired', - type: 'boolean', - description: - 'A DNS flag specifying that the client directs the server to pursue a query recursively. Recursive query support is optional.\n', - }, - { - name: 'flags.authentic_data', - type: 'boolean', - description: - 'A DNS flag specifying that the recursive server considers the response authentic.\n', - }, - { - name: 'flags.checking_disabled', - type: 'boolean', - description: - 'A DNS flag specifying that the client disables the server signature validation of the query.\n', - }, - { - name: 'flags.truncated_response', - type: 'boolean', - description: - 'A DNS flag specifying that only the first 512 bytes of the reply were returned.\n', - }, - { - name: 'question.etld_plus_one', - description: - 'The effective top-level domain (eTLD) plus one more label.\nFor example, the eTLD+1 for "foo.bar.golang.org." is "golang.org.".\nThe data for determining the eTLD comes from an embedded copy of the\ndata from http://publicsuffix.org.', - example: 'amazon.co.uk.', - }, - { - name: 'answers_count', - type: 'long', - description: 'The number of resource records contained in the `dns.answers` field.\n', - }, - { - name: 'authorities', - type: 'object', - description: - 'An array containing a dictionary for each authority section from the answer.\n', - }, - { - name: 'authorities_count', - type: 'long', - description: - 'The number of resource records contained in the `dns.authorities` field. The `dns.authorities` field may or may not be included depending on the configuration of Packetbeat.\n', - }, - { - name: 'authorities.name', - description: 'The domain name to which this resource record pertains.', - example: 'example.com.', - }, - { - name: 'authorities.type', - description: 'The type of data contained in this resource record.', - example: 'NS', - }, - { - name: 'authorities.class', - description: 'The class of DNS data contained in this resource record.', - example: 'IN', - }, - { - name: 'additionals', - type: 'object', - description: - 'An array containing a dictionary for each additional section from the answer.\n', - }, - { - name: 'additionals_count', - type: 'long', - description: - 'The number of resource records contained in the `dns.additionals` field. The `dns.additionals` field may or may not be included depending on the configuration of Packetbeat.\n', - }, - { - name: 'additionals.name', - description: 'The domain name to which this resource record pertains.', - example: 'example.com.', - }, - { - name: 'additionals.type', - description: 'The type of data contained in this resource record.', - example: 'NS', - }, - { - name: 'additionals.class', - description: 'The class of DNS data contained in this resource record.', - example: 'IN', - }, - { - name: 'additionals.ttl', - description: - 'The time interval in seconds that this resource record may be cached before it should be discarded. Zero values mean that the data should not be cached.\n', - type: 'long', - }, - { - name: 'additionals.data', - description: - 'The data describing the resource. The meaning of this data depends on the type and class of the resource record.\n', - }, - { - name: 'opt.version', - description: 'The EDNS version.', - example: '0', - }, - { - name: 'opt.do', - type: 'boolean', - description: 'If set, the transaction uses DNSSEC.', - }, - { - name: 'opt.ext_rcode', - description: 'Extended response code field.', - example: 'BADVERS', - }, - { - name: 'opt.udp_size', - type: 'long', - description: "Requestor's UDP payload size (in bytes).", - }, - ], - }, - ], - }, - { - key: 'http', - title: 'HTTP', - description: 'HTTP-specific event fields.', - fields: [ - { - name: 'http', - type: 'group', - description: 'Information about the HTTP request and response.', - fields: [ - { - name: 'request', - description: 'HTTP request', - type: 'group', - fields: [ - { - name: 'headers', - type: 'object', - object_type: 'keyword', - description: - 'A map containing the captured header fields from the request. Which headers to capture is configurable. If headers with the same header name are present in the message, they will be separated by commas.\n', - }, - { - name: 'params', - type: 'alias', - migration: true, - path: 'url.query', - }, - ], - }, - { - name: 'response', - description: 'HTTP response', - type: 'group', - fields: [ - { - name: 'status_phrase', - description: 'The HTTP status phrase.', - example: 'Not Found', - }, - { - name: 'headers', - type: 'object', - object_type: 'keyword', - description: - 'A map containing the captured header fields from the response. Which headers to capture is configurable. If headers with the same header name are present in the message, they will be separated by commas.\n', - }, - { - name: 'code', - type: 'alias', - migration: true, - path: 'http.response.status_code', - }, - { - name: 'phrase', - type: 'alias', - migration: true, - path: 'http.response.status_phrase', - }, - ], - }, - ], - }, - ], - }, - { - key: 'icmp', - title: 'ICMP', - description: 'ICMP specific event fields.\n', - fields: [ - { - name: 'icmp', - type: 'group', - fields: [ - { - name: 'version', - description: 'The version of the ICMP protocol.', - possible_values: [4, 6], - }, - { - name: 'request.message', - type: 'keyword', - description: 'A human readable form of the request.', - }, - { - name: 'request.type', - type: 'long', - description: 'The request type.', - }, - { - name: 'request.code', - type: 'long', - description: 'The request code.', - }, - { - name: 'response.message', - type: 'keyword', - description: 'A human readable form of the response.', - }, - { - name: 'response.type', - type: 'long', - description: 'The response type.', - }, - { - name: 'response.code', - type: 'long', - description: 'The response code.', - }, - ], - }, - ], - }, - { - key: 'memcache', - title: 'Memcache', - description: 'Memcached-specific event fields', - fields: [ - { - name: 'memcache', - type: 'group', - fields: [ - { - name: 'protocol_type', - type: 'keyword', - description: - 'The memcache protocol implementation. The value can be "binary" for binary-based, "text" for text-based, or "unknown" for an unknown memcache protocol type.\n', - }, - { - name: 'request.line', - type: 'keyword', - description: 'The raw command line for unknown commands ONLY.\n', - }, - { - name: 'request.command', - type: 'keyword', - description: - 'The memcache command being requested in the memcache text protocol. For example "set" or "get". The binary protocol opcodes are translated into memcache text protocol commands.\n', - }, - { - name: 'response.command', - type: 'keyword', - description: - 'Either the text based protocol response message type or the name of the originating request if binary protocol is used.\n', - }, - { - name: 'request.type', - type: 'keyword', - description: - 'The memcache command classification. This value can be "UNKNOWN", "Load", "Store", "Delete", "Counter", "Info", "SlabCtrl", "LRUCrawler", "Stats", "Success", "Fail", or "Auth".\n', - }, - { - name: 'response.type', - type: 'keyword', - description: - 'The memcache command classification. This value can be "UNKNOWN", "Load", "Store", "Delete", "Counter", "Info", "SlabCtrl", "LRUCrawler", "Stats", "Success", "Fail", or "Auth". The text based protocol will employ any of these, whereas the binary based protocol will mirror the request commands only (see `memcache.response.status` for binary protocol).\n', - }, - { - name: 'response.error_msg', - type: 'keyword', - description: - 'The optional error message in the memcache response (text based protocol only).\n', - }, - { - name: 'request.opcode', - type: 'keyword', - description: 'The binary protocol message opcode name.\n', - }, - { - name: 'response.opcode', - type: 'keyword', - description: 'The binary protocol message opcode name.\n', - }, - { - name: 'request.opcode_value', - type: 'long', - description: 'The binary protocol message opcode value.\n', - }, - { - name: 'response.opcode_value', - type: 'long', - description: 'The binary protocol message opcode value.\n', - }, - { - name: 'request.opaque', - type: 'long', - description: - 'The binary protocol opaque header value used for correlating request with response messages.\n', - }, - { - name: 'response.opaque', - type: 'long', - description: - 'The binary protocol opaque header value used for correlating request with response messages.\n', - }, - { - name: 'request.vbucket', - type: 'long', - description: 'The vbucket index sent in the binary message.\n', - }, - { - name: 'response.status', - type: 'keyword', - description: - 'The textual representation of the response error code (binary protocol only).\n', - }, - { - name: 'response.status_code', - type: 'long', - description: 'The status code value returned in the response (binary protocol only).\n', - }, - { - name: 'request.keys', - type: 'array', - description: 'The list of keys sent in the store or load commands.\n', - }, - { - name: 'response.keys', - type: 'array', - description: 'The list of keys returned for the load command (if present).\n', - }, - { - name: 'request.count_values', - type: 'long', - description: - 'The number of values found in the memcache request message. If the command does not send any data, this field is missing.\n', - }, - { - name: 'response.count_values', - type: 'long', - description: - 'The number of values found in the memcache response message. If the command does not send any data, this field is missing.\n', - }, - { - name: 'request.values', - type: 'array', - description: 'The list of base64 encoded values sent with the request (if present).\n', - }, - { - name: 'response.values', - type: 'array', - description: 'The list of base64 encoded values sent with the response (if present).\n', - }, - { - name: 'request.bytes', - type: 'long', - format: 'bytes', - description: 'The byte count of the values being transferred.\n', - }, - { - name: 'response.bytes', - type: 'long', - format: 'bytes', - description: 'The byte count of the values being transferred.\n', - }, - { - name: 'request.delta', - type: 'long', - description: 'The counter increment/decrement delta value.\n', - }, - { - name: 'request.initial', - type: 'long', - description: - 'The counter increment/decrement initial value parameter (binary protocol only).\n', - }, - { - name: 'request.verbosity', - type: 'long', - description: 'The value of the memcache "verbosity" command.\n', - }, - { - name: 'request.raw_args', - type: 'keyword', - description: - 'The text protocol raw arguments for the "stats ..." and "lru crawl ..." commands.\n', - }, - { - name: 'request.source_class', - type: 'long', - description: "The source class id in 'slab reassign' command.\n", - }, - { - name: 'request.dest_class', - type: 'long', - description: "The destination class id in 'slab reassign' command.\n", - }, - { - name: 'request.automove', - type: 'keyword', - description: - 'The automove mode in the \'slab automove\' command expressed as a string. This value can be "standby"(=0), "slow"(=1), "aggressive"(=2), or the raw value if the value is unknown.\n', - }, - { - name: 'request.flags', - type: 'long', - description: 'The memcache command flags sent in the request (if present).\n', - }, - { - name: 'response.flags', - type: 'long', - description: 'The memcache message flags sent in the response (if present).\n', - }, - { - name: 'request.exptime', - type: 'long', - description: - 'The data expiry time in seconds sent with the memcache command (if present). If the value is <30 days, the expiry time is relative to "now", or else it is an absolute Unix time in seconds (32-bit).\n', - }, - { - name: 'request.sleep_us', - type: 'long', - description: "The sleep setting in microseconds for the 'lru_crawler sleep' command.\n", - }, - { - name: 'response.value', - type: 'long', - description: 'The counter value returned by a counter operation.\n', - }, - { - name: 'request.noreply', - type: 'boolean', - description: - 'Set to true if noreply was set in the request. The `memcache.response` field will be missing.\n', - }, - { - name: 'request.quiet', - type: 'boolean', - description: - 'Set to true if the binary protocol message is to be treated as a quiet message.\n', - }, - { - name: 'request.cas_unique', - type: 'long', - description: 'The CAS (compare-and-swap) identifier if present.\n', - }, - { - name: 'response.cas_unique', - type: 'long', - description: - 'The CAS (compare-and-swap) identifier to be used with CAS-based updates (if present).\n', - }, - { - name: 'response.stats', - type: 'array', - description: - 'The list of statistic values returned. Each entry is a dictionary with the fields "name" and "value".\n', - }, - { - name: 'response.version', - type: 'keyword', - description: 'The returned memcache version string.\n', - }, - ], - }, - ], - }, - { - key: 'mongodb', - title: 'MongoDb', - description: - 'MongoDB-specific event fields. These fields mirror closely the fields for the MongoDB wire protocol. The higher level fields (for example, `query` and `resource`) apply to MongoDB events as well.\n', - fields: [ - { - name: 'mongodb', - type: 'group', - fields: [ - { - name: 'error', - description: - 'If the MongoDB request has resulted in an error, this field contains the error message returned by the server.\n', - }, - { - name: 'fullCollectionName', - description: - 'The full collection name. The full collection name is the concatenation of the database name with the collection name, using a dot (.) for the concatenation. For example, for the database foo and the collection bar, the full collection name is foo.bar.\n', - }, - { - name: 'numberToSkip', - type: 'long', - description: - 'Sets the number of documents to omit - starting from the first document in the resulting dataset - when returning the result of the query.\n', - }, - { - name: 'numberToReturn', - type: 'long', - description: 'The requested maximum number of documents to be returned.\n', - }, - { - name: 'numberReturned', - type: 'long', - description: 'The number of documents in the reply.\n', - }, - { - name: 'startingFrom', - description: 'Where in the cursor this reply is starting.\n', - }, - { - name: 'query', - description: - 'A JSON document that represents the query. The query will contain one or more elements, all of which must match for a document to be included in the result set. Possible elements include $query, $orderby, $hint, $explain, and $snapshot.\n', - }, - { - name: 'returnFieldsSelector', - description: - 'A JSON document that limits the fields in the returned documents. The returnFieldsSelector contains one or more elements, each of which is the name of a field that should be returned, and the integer value 1.\n', - }, - { - name: 'selector', - description: - 'A BSON document that specifies the query for selecting the document to update or delete.\n', - }, - { - name: 'update', - description: - 'A BSON document that specifies the update to be performed. For information on specifying updates, see the Update Operations documentation from the MongoDB Manual.\n', - }, - { - name: 'cursorId', - description: - 'The cursor identifier returned in the OP_REPLY. This must be the value that was returned from the database.\n', - }, - ], - }, - ], - }, - { - key: 'mysql', - title: 'MySQL', - description: 'MySQL-specific event fields.\n', - fields: [ - { - name: 'mysql', - type: 'group', - fields: [ - { - name: 'affected_rows', - type: 'long', - description: - 'If the MySQL command is successful, this field contains the affected number of rows of the last statement.\n', - }, - { - name: 'insert_id', - description: - 'If the INSERT query is successful, this field contains the id of the newly inserted row.\n', - }, - { - name: 'num_fields', - description: - 'If the SELECT query is successful, this field is set to the number of fields returned.\n', - }, - { - name: 'num_rows', - description: - 'If the SELECT query is successful, this field is set to the number of rows returned.\n', - }, - { - name: 'query', - description: "The row mysql query as read from the transaction's request.\n", - }, - { - name: 'error_code', - type: 'long', - description: 'The error code returned by MySQL.\n', - }, - { - name: 'error_message', - description: 'The error info message returned by MySQL.\n', - }, - ], - }, - ], - }, - { - key: 'nfs', - title: 'NFS', - description: 'NFS v4/3 specific event fields.', - fields: [ - { - name: 'nfs', - type: 'group', - fields: [ - { - name: 'version', - type: 'long', - description: 'NFS protocol version number.', - }, - { - name: 'minor_version', - type: 'long', - description: 'NFS protocol minor version number.', - }, - { - name: 'tag', - description: 'NFS v4 COMPOUND operation tag.', - }, - { - name: 'opcode', - description: 'NFS operation name, or main operation name, in case of COMPOUND calls.\n', - }, - { - name: 'status', - description: 'NFS operation reply status.', - }, - ], - }, - { - name: 'rpc', - type: 'group', - description: 'ONC RPC specific event fields.', - fields: [ - { - name: 'xid', - description: 'RPC message transaction identifier.', - }, - { - name: 'status', - description: 'RPC message reply status.', - }, - { - name: 'auth_flavor', - description: 'RPC authentication flavor.', - }, - { - name: 'cred.uid', - type: 'long', - description: "RPC caller's user id, in case of auth-unix.", - }, - { - name: 'cred.gid', - type: 'long', - description: "RPC caller's group id, in case of auth-unix.", - }, - { - name: 'cred.gids', - description: "RPC caller's secondary group ids, in case of auth-unix.", - }, - { - name: 'cred.stamp', - type: 'long', - description: 'Arbitrary ID which the caller machine may generate.', - }, - { - name: 'cred.machinename', - description: "The name of the caller's machine.", - }, - { - name: 'call_size', - type: 'alias', - path: 'source.bytes', - migration: true, - description: 'RPC call size with argument.', - }, - { - name: 'reply_size', - type: 'alias', - path: 'destination.bytes', - migration: true, - description: 'RPC reply size with argument.', - }, - ], - }, - ], - }, - { - key: 'pgsql', - title: 'PostgreSQL', - description: 'PostgreSQL-specific event fields.\n', - fields: [ - { - name: 'pgsql', - type: 'group', - fields: [ - { - name: 'error_code', - description: 'The PostgreSQL error code.', - type: 'long', - }, - { - name: 'error_message', - description: 'The PostgreSQL error message.', - }, - { - name: 'error_severity', - description: 'The PostgreSQL error severity.', - possible_values: ['ERROR', 'FATAL', 'PANIC'], - }, - { - name: 'num_fields', - description: - 'If the SELECT query if successful, this field is set to the number of fields returned.\n', - }, - { - name: 'num_rows', - description: - 'If the SELECT query if successful, this field is set to the number of rows returned.\n', - }, - ], - }, - ], - }, - { - key: 'redis', - title: 'Redis', - description: 'Redis-specific event fields.\n', - fields: [ - { - name: 'redis', - type: 'group', - fields: [ - { - name: 'return_value', - description: 'The return value of the Redis command in a human readable format.\n', - }, - { - name: 'error', - description: - 'If the Redis command has resulted in an error, this field contains the error message returned by the Redis server.\n', - }, - ], - }, - ], - }, - { - key: 'thrift', - title: 'Thrift-RPC', - description: 'Thrift-RPC specific event fields.\n', - fields: [ - { - name: 'thrift', - type: 'group', - fields: [ - { - name: 'params', - description: - 'The RPC method call parameters in a human readable format. If the IDL files are available, the parameters use names whenever possible. Otherwise, the IDs from the message are used.\n', - }, - { - name: 'service', - description: 'The name of the Thrift-RPC service as defined in the IDL files.\n', - }, - { - name: 'return_value', - description: - 'The value returned by the Thrift-RPC call. This is encoded in a human readable format.\n', - }, - { - name: 'exceptions', - description: - 'If the call resulted in exceptions, this field contains the exceptions in a human readable format.\n', - }, - ], - }, - ], - }, - { - key: 'tls_detailed', - title: 'Detailed TLS', - description: 'Detailed TLS-specific event fields.\n', - fields: [ - { - name: 'tls', - type: 'group', - fields: [ - { - name: 'detailed', - type: 'group', - default_fields: false, - fields: [ - { - name: 'version', - type: 'keyword', - description: 'The version of the TLS protocol used.\n', - example: 'TLS 1.3', - }, - { - name: 'resumption_method', - type: 'keyword', - description: - 'If the session has been resumed, the underlying method used. One of "id" for TLS session ID or "ticket" for TLS ticket extension.\n', - }, - { - name: 'client_certificate_requested', - type: 'boolean', - description: - 'Whether the server has requested the client to authenticate itself using a client certificate.\n', - }, - { - name: 'client_hello', - type: 'group', - fields: [ - { - name: 'version', - type: 'keyword', - description: - 'The version of the TLS protocol by which the client wishes to communicate during this session.\n', - }, - { - name: 'session_id', - type: 'keyword', - description: - 'Unique number to identify the session for the corresponding connection with the client.\n', - }, - { - name: 'supported_compression_methods', - type: 'keyword', - description: - 'The list of compression methods the client supports. See https://www.iana.org/assignments/comp-meth-ids/comp-meth-ids.xhtml\n', - }, - { - name: 'extensions', - type: 'group', - description: 'The hello extensions provided by the client.', - fields: [ - { - name: 'server_name_indication', - type: 'keyword', - description: 'List of hostnames', - }, - { - name: 'application_layer_protocol_negotiation', - type: 'keyword', - description: - 'List of application-layer protocols the client is willing to use.\n', - }, - { - name: 'session_ticket', - type: 'keyword', - description: - 'Length of the session ticket, if provided, or an empty string to advertise support for tickets.\n', - }, - { - name: 'supported_versions', - type: 'keyword', - description: 'List of TLS versions that the client is willing to use.\n', - }, - { - name: 'supported_groups', - type: 'keyword', - description: - 'List of Elliptic Curve Cryptography (ECC) curve groups supported by the client.\n', - }, - { - name: 'signature_algorithms', - type: 'keyword', - description: - 'List of signature algorithms that may be use in digital signatures.\n', - }, - { - name: 'ec_points_formats', - type: 'keyword', - description: - 'List of Elliptic Curve (EC) point formats. Indicates the set of point formats that the client can parse.\n', - }, - { - name: '_unparsed_', - type: 'keyword', - description: 'List of extensions that were left unparsed by Packetbeat.\n', - }, - ], - }, - ], - }, - { - name: 'server_hello', - type: 'group', - fields: [ - { - name: 'version', - type: 'keyword', - description: - 'The version of the TLS protocol that is used for this session. It is the highest version supported by the server not exceeding the version requested in the client hello.\n', - }, - { - name: 'selected_compression_method', - type: 'keyword', - description: - 'The compression method selected by the server from the list provided in the client hello.\n', - }, - { - name: 'session_id', - type: 'keyword', - description: - 'Unique number to identify the session for the corresponding connection with the client.\n', - }, - { - name: 'extensions', - type: 'group', - description: 'The hello extensions provided by the server.', - fields: [ - { - name: 'application_layer_protocol_negotiation', - type: 'keyword', - description: 'Negotiated application layer protocol', - }, - { - name: 'session_ticket', - type: 'keyword', - description: - 'Used to announce that a session ticket will be provided by the server. Always an empty string.\n', - }, - { - name: 'supported_versions', - type: 'keyword', - description: 'Negotiated TLS version to be used.\n', - }, - { - name: 'ec_points_formats', - type: 'keyword', - description: - 'List of Elliptic Curve (EC) point formats. Indicates the set of point formats that the server can parse.\n', - }, - { - name: '_unparsed_', - type: 'keyword', - description: 'List of extensions that were left unparsed by Packetbeat.\n', - }, - ], - }, - ], - }, - { - name: 'client_certificate', - type: 'group', - description: 'Certificate provided by the client for authentication.', - fields: [ - { - name: 'version', - type: 'long', - description: 'X509 format version.', - }, - { - name: 'serial_number', - type: 'keyword', - description: "The certificate's serial number.", - }, - { - name: 'not_before', - type: 'date', - description: 'Date before which the certificate is not valid.', - }, - { - name: 'not_after', - type: 'date', - description: 'Date after which the certificate expires.', - }, - { - name: 'public_key_algorithm', - type: 'keyword', - description: - "The algorithm used for this certificate's public key. One of RSA, DSA or ECDSA.\n", - }, - { - name: 'public_key_size', - type: 'long', - description: 'Size of the public key.', - }, - { - name: 'signature_algorithm', - type: 'keyword', - description: "The algorithm used for the certificate's signature.\n", - }, - { - name: 'alternative_names', - type: 'keyword', - description: 'Subject Alternative Names for this certificate.', - }, - { - name: 'subject', - type: 'group', - description: 'Subject represented by this certificate.', - fields: [ - { - name: 'country', - type: 'keyword', - description: 'Country code.', - }, - { - name: 'organization', - type: 'keyword', - description: 'Organization name.', - }, - { - name: 'organizational_unit', - type: 'keyword', - description: 'Unit within organization.', - }, - { - name: 'province', - type: 'keyword', - description: 'Province or region within country.', - }, - { - name: 'common_name', - type: 'keyword', - description: 'Name or host name identified by the certificate.', - }, - { - name: 'locality', - type: 'keyword', - description: 'Locality.', - }, - ], - }, - { - name: 'issuer', - type: 'group', - description: 'Entity that issued and signed this certificate.', - fields: [ - { - name: 'country', - type: 'keyword', - description: 'Country code.', - }, - { - name: 'organization', - type: 'keyword', - description: 'Organization name.', - }, - { - name: 'organizational_unit', - type: 'keyword', - description: 'Unit within organization.', - }, - { - name: 'province', - type: 'keyword', - description: 'Province or region within country.', - }, - { - name: 'common_name', - type: 'keyword', - description: 'Name or host name identified by the certificate.', - }, - { - name: 'locality', - type: 'keyword', - description: 'Locality.', - }, - ], - }, - ], - }, - { - name: 'server_certificate', - type: 'group', - description: 'Certificate provided by the server for authentication.', - fields: [ - { - name: 'version', - type: 'long', - description: 'X509 format version.', - }, - { - name: 'serial_number', - type: 'keyword', - description: "The certificate's serial number.", - }, - { - name: 'not_before', - type: 'date', - description: 'Date before which the certificate is not valid.', - }, - { - name: 'not_after', - type: 'date', - description: 'Date after which the certificate expires.', - }, - { - name: 'public_key_algorithm', - type: 'keyword', - description: - "The algorithm used for this certificate's public key. One of RSA, DSA or ECDSA.\n", - }, - { - name: 'public_key_size', - type: 'long', - description: 'Size of the public key.', - }, - { - name: 'signature_algorithm', - type: 'keyword', - description: "The algorithm used for the certificate's signature.\n", - }, - { - name: 'alternative_names', - type: 'keyword', - description: 'Subject Alternative Names for this certificate.', - }, - { - name: 'subject', - type: 'group', - description: 'Subject represented by this certificate.', - fields: [ - { - name: 'country', - type: 'keyword', - description: 'Country code.', - }, - { - name: 'organization', - type: 'keyword', - description: 'Organization name.', - }, - { - name: 'organizational_unit', - type: 'keyword', - description: 'Unit within organization.', - }, - { - name: 'province', - type: 'keyword', - description: 'Province or region within country.', - }, - { - name: 'common_name', - type: 'keyword', - description: 'Name or host name identified by the certificate.', - }, - { - name: 'locality', - type: 'keyword', - description: 'Locality.', - }, - ], - }, - { - name: 'issuer', - type: 'group', - description: 'Entity that issued and signed this certificate.', - fields: [ - { - name: 'country', - type: 'keyword', - description: 'Country code.', - }, - { - name: 'organization', - type: 'keyword', - description: 'Organization name.', - }, - { - name: 'organizational_unit', - type: 'keyword', - description: 'Unit within organization.', - }, - { - name: 'province', - type: 'keyword', - description: 'Province or region within country.', - }, - { - name: 'common_name', - type: 'keyword', - description: 'Name or host name identified by the certificate.', - }, - { - name: 'locality', - type: 'keyword', - description: 'Locality.', - }, - ], - }, - ], - }, - { - name: 'server_certificate_chain', - type: 'array', - description: 'Chain of trust for the server certificate.', - }, - { - name: 'client_certificate_chain', - type: 'array', - description: 'Chain of trust for the client certificate.', - }, - { - name: 'alert_types', - type: 'keyword', - description: 'An array containing the TLS alert type for every alert received.\n', - }, - ], - }, - ], - }, - { - name: 'tls.handshake_completed', - type: 'alias', - path: 'tls.established', - }, - { - name: 'tls.client_hello.supported_ciphers', - type: 'alias', - path: 'tls.client.supported_ciphers', - }, - { - name: 'tls.server_hello.selected_cipher', - type: 'alias', - path: 'tls.cipher', - }, - { - name: 'tls.fingerprints.ja3', - type: 'alias', - path: 'tls.client.ja3', - }, - { - name: 'tls.resumption_method', - type: 'alias', - path: 'tls.detailed.resumption_method', - }, - { - name: 'tls.client_certificate_requested', - type: 'alias', - path: 'tls.detailed.client_certificate_requested', - }, - { - name: 'tls.client_hello.version', - type: 'alias', - path: 'tls.detailed.client_hello.version', - }, - { - name: 'tls.client_hello.session_id', - type: 'alias', - path: 'tls.detailed.client_hello.session_id', - }, - { - name: 'tls.client_hello.supported_compression_methods', - type: 'alias', - path: 'tls.detailed.client_hello.supported_compression_methods', - }, - { - name: 'tls.client_hello.extensions.server_name_indication', - type: 'alias', - path: 'tls.detailed.client_hello.extensions.server_name_indication', - }, - { - name: 'tls.client_hello.extensions.application_layer_protocol_negotiation', - type: 'alias', - path: 'tls.detailed.client_hello.extensions.application_layer_protocol_negotiation', - }, - { - name: 'tls.client_hello.extensions.session_ticket', - type: 'alias', - path: 'tls.detailed.client_hello.extensions.session_ticket', - }, - { - name: 'tls.client_hello.extensions.supported_versions', - type: 'alias', - path: 'tls.detailed.client_hello.extensions.supported_versions', - }, - { - name: 'tls.client_hello.extensions.supported_groups', - type: 'alias', - path: 'tls.detailed.client_hello.extensions.supported_groups', - }, - { - name: 'tls.client_hello.extensions.signature_algorithms', - type: 'alias', - path: 'tls.detailed.client_hello.extensions.signature_algorithms', - }, - { - name: 'tls.client_hello.extensions.ec_points_formats', - type: 'alias', - path: 'tls.detailed.client_hello.extensions.ec_points_formats', - }, - { - name: 'tls.client_hello.extensions._unparsed_', - type: 'alias', - path: 'tls.detailed.client_hello.extensions._unparsed_', - }, - { - name: 'tls.server_hello.version', - type: 'alias', - path: 'tls.detailed.server_hello.version', - }, - { - name: 'tls.server_hello.selected_compression_method', - type: 'alias', - path: 'tls.detailed.server_hello.selected_compression_method', - }, - { - name: 'tls.server_hello.session_id', - type: 'alias', - path: 'tls.detailed.server_hello.session_id', - }, - { - name: 'tls.server_hello.extensions.application_layer_protocol_negotiation', - type: 'alias', - path: 'tls.detailed.server_hello.extensions.application_layer_protocol_negotiation', - }, - { - name: 'tls.server_hello.extensions.session_ticket', - type: 'alias', - path: 'tls.detailed.server_hello.extensions.session_ticket', - }, - { - name: 'tls.server_hello.extensions.supported_versions', - type: 'alias', - path: 'tls.detailed.server_hello.extensions.supported_versions', - }, - { - name: 'tls.server_hello.extensions.ec_points_formats', - type: 'alias', - path: 'tls.detailed.server_hello.extensions.ec_points_formats', - }, - { - name: 'tls.server_hello.extensions._unparsed_', - type: 'alias', - path: 'tls.detailed.server_hello.extensions._unparsed_', - }, - { - name: 'tls.client_certificate.version', - type: 'alias', - path: 'tls.detailed.client_certificate.version', - }, - { - name: 'tls.client_certificate.serial_number', - type: 'alias', - path: 'tls.detailed.client_certificate.serial_number', - }, - { - name: 'tls.client_certificate.not_before', - type: 'alias', - path: 'tls.detailed.client_certificate.not_before', - }, - { - name: 'tls.client_certificate.not_after', - type: 'alias', - path: 'tls.detailed.client_certificate.not_after', - }, - { - name: 'tls.client_certificate.public_key_algorithm', - type: 'alias', - path: 'tls.detailed.client_certificate.public_key_algorithm', - }, - { - name: 'tls.client_certificate.public_key_size', - type: 'alias', - path: 'tls.detailed.client_certificate.public_key_size', - }, - { - name: 'tls.client_certificate.signature_algorithm', - type: 'alias', - path: 'tls.detailed.client_certificate.signature_algorithm', - }, - { - name: 'tls.client_certificate.alternative_names', - type: 'alias', - path: 'tls.detailed.client_certificate.alternative_names', - }, - { - name: 'tls.client_certificate.subject.country', - type: 'alias', - path: 'tls.detailed.client_certificate.subject.country', - }, - { - name: 'tls.client_certificate.subject.organization', - type: 'alias', - path: 'tls.detailed.client_certificate.subject.organization', - }, - { - name: 'tls.client_certificate.subject.organizational_unit', - type: 'alias', - path: 'tls.detailed.client_certificate.subject.organizational_unit', - }, - { - name: 'tls.client_certificate.subject.province', - type: 'alias', - path: 'tls.detailed.client_certificate.subject.province', - }, - { - name: 'tls.client_certificate.subject.common_name', - type: 'alias', - path: 'tls.detailed.client_certificate.subject.common_name', - }, - { - name: 'tls.client_certificate.subject.locality', - type: 'alias', - path: 'tls.detailed.client_certificate.subject.locality', - }, - { - name: 'tls.client_certificate.issuer.country', - type: 'alias', - path: 'tls.detailed.client_certificate.issuer.country', - }, - { - name: 'tls.client_certificate.issuer.organization', - type: 'alias', - path: 'tls.detailed.client_certificate.issuer.organization', - }, - { - name: 'tls.client_certificate.issuer.organizational_unit', - type: 'alias', - path: 'tls.detailed.client_certificate.issuer.organizational_unit', - }, - { - name: 'tls.client_certificate.issuer.province', - type: 'alias', - path: 'tls.detailed.client_certificate.issuer.province', - }, - { - name: 'tls.client_certificate.issuer.common_name', - type: 'alias', - path: 'tls.detailed.client_certificate.issuer.common_name', - }, - { - name: 'tls.client_certificate.issuer.locality', - type: 'alias', - path: 'tls.detailed.client_certificate.issuer.locality', - }, - { - name: 'tls.server_certificate.version', - type: 'alias', - path: 'tls.detailed.server_certificate.version', - }, - { - name: 'tls.server_certificate.serial_number', - type: 'alias', - path: 'tls.detailed.server_certificate.serial_number', - }, - { - name: 'tls.server_certificate.not_before', - type: 'alias', - path: 'tls.detailed.server_certificate.not_before', - }, - { - name: 'tls.server_certificate.not_after', - type: 'alias', - path: 'tls.detailed.server_certificate.not_after', - }, - { - name: 'tls.server_certificate.public_key_algorithm', - type: 'alias', - path: 'tls.detailed.server_certificate.public_key_algorithm', - }, - { - name: 'tls.server_certificate.public_key_size', - type: 'alias', - path: 'tls.detailed.server_certificate.public_key_size', - }, - { - name: 'tls.server_certificate.signature_algorithm', - type: 'alias', - path: 'tls.detailed.server_certificate.signature_algorithm', - }, - { - name: 'tls.server_certificate.alternative_names', - type: 'alias', - path: 'tls.detailed.server_certificate.alternative_names', - }, - { - name: 'tls.server_certificate.subject.country', - type: 'alias', - path: 'tls.detailed.server_certificate.subject.country', - }, - { - name: 'tls.server_certificate.subject.organization', - type: 'alias', - path: 'tls.detailed.server_certificate.subject.organization', - }, - { - name: 'tls.server_certificate.subject.organizational_unit', - type: 'alias', - path: 'tls.detailed.server_certificate.subject.organizational_unit', - }, - { - name: 'tls.server_certificate.subject.province', - type: 'alias', - path: 'tls.detailed.server_certificate.subject.province', - }, - { - name: 'tls.server_certificate.subject.common_name', - type: 'alias', - path: 'tls.detailed.server_certificate.subject.common_name', - }, - { - name: 'tls.server_certificate.subject.locality', - type: 'alias', - path: 'tls.detailed.server_certificate.subject.locality', - }, - { - name: 'tls.server_certificate.issuer.country', - type: 'alias', - path: 'tls.detailed.server_certificate.issuer.country', - }, - { - name: 'tls.server_certificate.issuer.organization', - type: 'alias', - path: 'tls.detailed.server_certificate.issuer.organization', - }, - { - name: 'tls.server_certificate.issuer.organizational_unit', - type: 'alias', - path: 'tls.detailed.server_certificate.issuer.organizational_unit', - }, - { - name: 'tls.server_certificate.issuer.province', - type: 'alias', - path: 'tls.detailed.server_certificate.issuer.province', - }, - { - name: 'tls.server_certificate.issuer.common_name', - type: 'alias', - path: 'tls.detailed.server_certificate.issuer.common_name', - }, - { - name: 'tls.server_certificate.issuer.locality', - type: 'alias', - path: 'tls.detailed.server_certificate.issuer.locality', - }, - { - name: 'tls.alert_types', - type: 'alias', - path: 'tls.detailed.alert_types', - }, - ], - }, -]; diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/winlogbeat.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/winlogbeat.ts deleted file mode 100644 index 7457cb3f4428f..0000000000000 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/8.0.0/winlogbeat.ts +++ /dev/null @@ -1,2844 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -/** - * An instance of the unmodified schema exported from winlogbeat-8.0.0-SNAPSHOT-windows-x86_64.zip - * - */ - -import { Schema } from '../type'; - -export const winlogbeatSchema: Schema = [ - { - key: 'ecs', - title: 'ECS', - description: 'ECS Fields.', - fields: [ - { - name: '@timestamp', - level: 'core', - required: true, - type: 'date', - description: - 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', - example: '2016-05-23T08:05:34.853Z', - }, - { - name: 'labels', - level: 'core', - type: 'object', - object_type: 'keyword', - description: - 'Custom key/value pairs.\n\nCan be used to add meta information to events. Should not contain nested objects.\nAll values are stored as keyword.\n\nExample: `docker` and `k8s` labels.', - example: { - application: 'foo-bar', - env: 'production', - }, - }, - { - name: 'message', - level: 'core', - type: 'text', - description: - 'For log events the message field contains the log message, optimized\nfor viewing in a log viewer.\n\nFor structured logs without an original message field, other fields can be concatenated\nto form a human-readable summary of the event.\n\nIf multiple messages exist, they can be combined into one message.', - example: 'Hello World', - }, - { - name: 'tags', - level: 'core', - type: 'keyword', - description: 'List of keywords used to tag each event.', - example: '["production", "env2"]', - }, - { - name: 'agent', - title: 'Agent', - group: 2, - description: - 'The agent fields contain the data about the software entity, if\nany, that collects, detects, or observes events on a host, or takes measurements\non a host.\n\nExamples include Beats. Agents may also run on observers. ECS agent.* fields\nshall be populated with details of the agent running on the host or observer\nwhere the event happened or the measurement was taken.', - footnote: - 'Examples: In the case of Beats for logs, the agent.name is filebeat.\nFor APM, it is the agent running in the app/service. The agent information does\nnot change if data is sent through queuing systems like Kafka, Redis, or processing\nsystems such as Logstash or APM Server.', - type: 'group', - fields: [ - { - name: 'ephemeral_id', - level: 'extended', - type: 'keyword', - description: - 'Ephemeral identifier of this agent (if one exists).\n\nThis id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - description: - 'Unique identifier of this agent (if one exists).\n\nExample: For Beats this would be beat.id.', - example: '8a4f500d', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - description: - 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', - example: 'foo', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - description: - 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', - example: 'filebeat', - }, - { - name: 'version', - level: 'core', - type: 'keyword', - description: 'Version of the agent.', - example: '6.0.0-rc2', - }, - ], - }, - { - name: 'client', - title: 'Client', - group: 2, - description: - 'A client is defined as the initiator of a network connection for\nevents regarding sessions, connections, or bidirectional flow records.\n\nFor TCP events, the client is the initiator of the TCP connection that sends\nthe SYN packet(s). For other protocols, the client is generally the initiator\nor requestor in the network transaction. Some systems use the term "originator"\nto refer the client in TCP connections. The client fields describe details about\nthe system acting as the client in the network event. Client fields are usually\npopulated in conjunction with server fields. Client fields are generally not\npopulated for packet-level events.\n\nClient / server representations can add semantic context to an exchange, which\nis helpful to visualize the data in certain situations. If your context falls\nin that category, you should still ensure that source and destination are filled\nappropriately.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - description: - 'Some event client addresses are defined ambiguously. The event\nwill sometimes list an IP, a domain or a unix socket. You should always store\nthe raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the client to the server.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - description: 'Client domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: - 'IP address of the client.\n\nCan be one or multiple IPv4 or IPv6 addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - description: 'MAC address of the client.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the client to the server.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the client.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - description: 'One or multiple unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'cloud', - title: 'Cloud', - group: 2, - description: 'Fields related to the cloud or infrastructure the events are coming\nfrom.', - footnote: - 'Examples: If Metricbeat is running on an EC2 host and fetches data\nfrom its host, the cloud info contains the data about this machine. If Metricbeat\nruns on a remote machine outside the cloud and fetches data from a service running\nin the cloud, the field contains cloud data from the machine the service is\nrunning on.', - type: 'group', - fields: [ - { - name: 'account.id', - level: 'extended', - type: 'keyword', - description: - 'The cloud account or organization id used to identify different\nentities in a multi-tenant environment.\n\nExamples: AWS account id, Google Cloud ORG Id, or other unique identifier.', - example: 666777888999, - }, - { - name: 'availability_zone', - level: 'extended', - type: 'keyword', - description: 'Availability zone in which this host is running.', - example: 'us-east-1c', - }, - { - name: 'instance.id', - level: 'extended', - type: 'keyword', - description: 'Instance ID of the host machine.', - example: 'i-1234567890abcdef0', - }, - { - name: 'instance.name', - level: 'extended', - type: 'keyword', - description: 'Instance name of the host machine.', - }, - { - name: 'machine.type', - level: 'extended', - type: 'keyword', - description: 'Machine type of the host machine.', - example: 't2.medium', - }, - { - name: 'provider', - level: 'extended', - type: 'keyword', - description: - 'Name of the cloud provider. Example values are aws, azure, gcp,\nor digitalocean.', - example: 'aws', - }, - { - name: 'region', - level: 'extended', - type: 'keyword', - description: 'Region in which this host is running.', - example: 'us-east-1', - }, - ], - }, - { - name: 'container', - title: 'Container', - group: 2, - description: - 'Container fields are used for meta information about the specific\ncontainer that is the source of information.\n\nThese fields help correlate data based containers from any runtime.', - type: 'group', - fields: [ - { - name: 'id', - level: 'core', - type: 'keyword', - description: 'Unique container id.', - }, - { - name: 'image.name', - level: 'extended', - type: 'keyword', - description: 'Name of the image the container was built on.', - }, - { - name: 'image.tag', - level: 'extended', - type: 'keyword', - description: 'Container image tag.', - }, - { - name: 'labels', - level: 'extended', - type: 'object', - object_type: 'keyword', - description: 'Image labels.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - description: 'Container name.', - }, - { - name: 'runtime', - level: 'extended', - type: 'keyword', - description: 'Runtime managing this container.', - example: 'docker', - }, - ], - }, - { - name: 'destination', - title: 'Destination', - group: 2, - description: - 'Destination fields describe details about the destination of a packet/event.\n\nDestination fields are usually populated in conjunction with source fields.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - description: - 'Some event destination addresses are defined ambiguously. The\nevent will sometimes list an IP, a domain or a unix socket. You should always\nstore the raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the destination to the source.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - description: 'Destination domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: - 'IP address of the destination.\n\nCan be one or multiple IPv4 or IPv6 addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - description: 'MAC address of the destination.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the destination to the source.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the destination.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - description: 'One or multiple unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'ecs', - title: 'ECS', - group: 2, - description: 'Meta-information specific to ECS.', - type: 'group', - fields: [ - { - name: 'version', - level: 'core', - required: true, - type: 'keyword', - description: - 'ECS version this event conforms to. `ecs.version` is a required\nfield and must exist in all events.\n\nWhen querying across multiple indices -- which may conform to slightly different\nECS versions -- this field lets integrations adjust to the schema version\nof the events.', - example: '1.0.0', - }, - ], - }, - { - name: 'error', - title: 'Error', - group: 2, - description: - 'These fields can represent errors of any kind.\n\nUse them for errors that happen while fetching events or in cases where the\nevent itself contains an error.', - type: 'group', - fields: [ - { - name: 'code', - level: 'core', - type: 'keyword', - description: 'Error code describing the error.', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - description: 'Unique identifier for the error.', - }, - { - name: 'message', - level: 'core', - type: 'text', - description: 'Error message.', - }, - ], - }, - { - name: 'event', - title: 'Event', - group: 2, - description: - 'The event fields are used for context information about the log\nor metric event itself.\n\nA log is defined as an event containing details of something that happened.\nLog events must include the time at which the thing happened. Examples of log\nevents include a process starting on a host, a network packet being sent from\na source to a destination, or a network connection between a client and a server\nbeing initiated or closed. A metric is defined as an event containing one or\nmore numerical or categorical measurements and the time at which the measurement\nwas taken. Examples of metric events include memory pressure measured on a host,\nor vulnerabilities measured on a scanned host.', - type: 'group', - fields: [ - { - name: 'action', - level: 'core', - type: 'keyword', - description: - 'The action captured by the event.\n\nThis describes the information in the event. It is more specific than `event.category`.\nExamples are `group-add`, `process-started`, `file-created`. The value is\nnormally defined by the implementer.', - example: 'user-password-change', - }, - { - name: 'category', - level: 'core', - type: 'keyword', - description: - 'Event category.\n\nThis contains high-level information about the contents of the event. It is\nmore generic than `event.action`, in the sense that typically a category contains\nmultiple actions. Warning: In future versions of ECS, we plan to provide a\nlist of acceptable values for this field, please use with caution.', - example: 'user-management', - }, - { - name: 'created', - level: 'core', - type: 'date', - description: - 'event.created contains the date/time when the event was first\nread by an agent, or by your pipeline.\n\nThis field is distinct from @timestamp in that @timestamp typically contain\nthe time extracted from the original event.\n\nIn most situations, these two timestamps will be slightly different. The difference\ncan be used to calculate the delay between your source generating an event,\nand the time when your agent first processed it. This can be used to monitor\nyour agents or pipelines ability to keep up with your event source.\n\nIn case the two timestamps are identical, @timestamp should be used.', - }, - { - name: 'dataset', - level: 'core', - type: 'keyword', - description: - 'Name of the dataset.\n\nThe concept of a `dataset` (fileset / metricset) is used in Beats as a subset\nof modules. It contains the information which is currently stored in metricset.name\nand metricset.module or fileset.name.', - example: 'stats', - }, - { - name: 'duration', - level: 'core', - type: 'long', - format: 'duration', - input_format: 'nanoseconds', - description: - 'Duration of the event in nanoseconds.\n\nIf event.start and event.end are known this value should be the difference\nbetween the end and start time.', - }, - { - name: 'end', - level: 'extended', - type: 'date', - description: - 'event.end contains the date when the event ended or when the activity\nwas last observed.', - }, - { - name: 'hash', - level: 'extended', - type: 'keyword', - description: - 'Hash (perhaps logstash fingerprint) of raw field to be able to\ndemonstrate log integrity.', - example: '123456789012345678901234567890ABCD', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - description: 'Unique ID to describe the event.', - example: '8a4f500d', - }, - { - name: 'kind', - level: 'extended', - type: 'keyword', - description: - 'The kind of the event.\n\nThis gives information about what type of information the event contains,\nwithout being specific to the contents of the event. Examples are `event`,\n`state`, `alarm`. Warning: In future versions of ECS, we plan to provide a\nlist of acceptable values for this field, please use with caution.', - example: 'state', - }, - { - name: 'module', - level: 'core', - type: 'keyword', - description: - 'Name of the module this data is coming from.\n\nThis information is coming from the modules used in Beats or Logstash.', - example: 'mysql', - }, - { - name: 'original', - level: 'core', - type: 'keyword', - description: - 'Raw text message of entire event. Used to demonstrate log integrity.\n\nThis field is not indexed and doc_values are disabled. It cannot be searched,\nbut it can be retrieved from `_source`.', - example: - 'Sep 19 08:26:10 host CEF:0|Security| threatmanager|1.0|100|\nworm successfully stopped|10|src=10.0.0.1 dst=2.1.2.2spt=1232', - }, - { - name: 'outcome', - level: 'extended', - type: 'keyword', - description: - 'The outcome of the event.\n\nIf the event describes an action, this fields contains the outcome of that\naction. Examples outcomes are `success` and `failure`. Warning: In future\nversions of ECS, we plan to provide a list of acceptable values for this field,\nplease use with caution.', - example: 'success', - }, - { - name: 'risk_score', - level: 'core', - type: 'float', - description: - "Risk score or priority of the event (e.g. security solutions).\nUse your system's original value here.", - }, - { - name: 'risk_score_norm', - level: 'extended', - type: 'float', - description: - 'Normalized risk score or priority of the event, on a scale of\n0 to 100.\n\nThis is mainly useful if you use more than one system that assigns risk scores,\nand you want to see a normalized value across all systems.', - }, - { - name: 'severity', - level: 'core', - type: 'long', - format: 'string', - description: - "Severity describes the original severity of the event. What the\ndifferent severity values mean can very different between use cases. It's\nup to the implementer to make sure severities are consistent across events.", - example: '7', - }, - { - name: 'start', - level: 'extended', - type: 'date', - description: - 'event.start contains the date when the event started or when the\nactivity was first observed.', - }, - { - name: 'timezone', - level: 'extended', - type: 'keyword', - description: - 'This field should be populated when the events timestamp does\nnot include timezone information already (e.g. default Syslog timestamps).\nIts optional otherwise.\n\nAcceptable timezone formats are: a canonical ID (e.g. "Europe/Amsterdam"),\nabbreviated (e.g. "EST") or an HH:mm differential (e.g. "-05:00").', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - description: - 'Reserved for future usage.\n\nPlease avoid using this field for user data.', - }, - ], - }, - { - name: 'file', - title: 'File', - group: 2, - description: - 'A file is defined as a set of information that has been created\non, or has existed on a filesystem.\n\nFile objects can be associated with host events, network events, and/or file\nevents (e.g., those produced by File Integrity Monitoring [FIM] products or\nservices). File fields provide details about the affected file associated with\nthe event or metric.', - type: 'group', - fields: [ - { - name: 'ctime', - level: 'extended', - type: 'date', - description: 'Last time file metadata changed.', - }, - { - name: 'device', - level: 'extended', - type: 'keyword', - description: 'Device that is the source of the file.', - }, - { - name: 'extension', - level: 'extended', - type: 'keyword', - description: 'File extension.\n\nThis should allow easy filtering by file extensions.', - example: 'png', - }, - { - name: 'gid', - level: 'extended', - type: 'keyword', - description: 'Primary group ID (GID) of the file.', - }, - { - name: 'group', - level: 'extended', - type: 'keyword', - description: 'Primary group name of the file.', - }, - { - name: 'inode', - level: 'extended', - type: 'keyword', - description: 'Inode representing the file in the filesystem.', - }, - { - name: 'mode', - level: 'extended', - type: 'keyword', - description: 'Mode of the file in octal representation.', - example: 416, - }, - { - name: 'mtime', - level: 'extended', - type: 'date', - description: 'Last time file content was modified.', - }, - { - name: 'owner', - level: 'extended', - type: 'keyword', - description: "File owner's username.", - }, - { - name: 'path', - level: 'extended', - type: 'keyword', - description: 'Path to the file.', - }, - { - name: 'size', - level: 'extended', - type: 'long', - description: 'File size in bytes (field is only added when `type` is `file`).', - }, - { - name: 'target_path', - level: 'extended', - type: 'keyword', - description: 'Target path for symlinks.', - }, - { - name: 'type', - level: 'extended', - type: 'keyword', - description: 'File type (file, dir, or symlink).', - }, - { - name: 'uid', - level: 'extended', - type: 'keyword', - description: 'The user ID (UID) or security identifier (SID) of the file owner.', - }, - ], - }, - { - name: 'geo', - title: 'Geo', - group: 2, - description: - 'Geo fields can carry data about a specific location related to an\nevent.\n\nThis geolocation information can be derived from techniques such as Geo IP,\nor be user-supplied.', - type: 'group', - fields: [ - { - name: 'city_name', - level: 'core', - type: 'keyword', - description: 'City name.', - example: 'Montreal', - }, - { - name: 'continent_name', - level: 'core', - type: 'keyword', - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'country_iso_code', - level: 'core', - type: 'keyword', - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'country_name', - level: 'core', - type: 'keyword', - description: 'Country name.', - example: 'Canada', - }, - { - name: 'location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'region_iso_code', - level: 'core', - type: 'keyword', - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'region_name', - level: 'core', - type: 'keyword', - description: 'Region name.', - example: 'Quebec', - }, - ], - }, - { - name: 'group', - title: 'Group', - group: 2, - description: - 'The group fields are meant to represent groups that are relevant\nto the event.', - type: 'group', - fields: [ - { - name: 'id', - level: 'extended', - type: 'keyword', - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - description: 'Name of the group.', - }, - ], - }, - { - name: 'host', - title: 'Host', - group: 2, - description: - 'A host is defined as a general computing instance.\n\nECS host.* fields should be populated with details about the host on which the\nevent happened, or from which the measurement was taken. Host types include\nhardware, virtual machines, Docker containers, and Kubernetes nodes.', - type: 'group', - fields: [ - { - name: 'architecture', - level: 'core', - type: 'keyword', - description: 'Operating system architecture.', - example: 'x86_64', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'hostname', - level: 'core', - type: 'keyword', - description: - 'Hostname of the host.\n\nIt normally contains what the `hostname` command returns on the host machine.', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - description: - 'Unique host id.\n\nAs hostname is not always unique, use values that are meaningful in your environment.\n\nExample: The current usage of `beat.name`.', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: 'Host ip address.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - description: 'Host mac address.', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - description: - 'Name of the host.\n\nIt can contain what `hostname` returns on Unix systems, the fully qualified\ndomain name, or a name specified by the user. The sender decides which value\nto use.', - }, - { - name: 'os.family', - level: 'extended', - type: 'keyword', - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'os.full', - level: 'extended', - type: 'keyword', - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'os.kernel', - level: 'extended', - type: 'keyword', - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'os.name', - level: 'extended', - type: 'keyword', - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'os.platform', - level: 'extended', - type: 'keyword', - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'os.version', - level: 'extended', - type: 'keyword', - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - description: - 'Type of host.\n\nFor Cloud providers this can be the machine type like `t2.medium`. If vm,\nthis could be the container, for example, or other information meaningful\nin your environment.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - description: 'One or multiple unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'http', - title: 'HTTP', - group: 2, - description: - 'Fields related to HTTP activity. Use the `url` field set to store\nthe url of the request.', - type: 'group', - fields: [ - { - name: 'request.body.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Size in bytes of the request body.', - example: 887, - }, - { - name: 'request.body.content', - level: 'extended', - type: 'keyword', - description: 'The full HTTP request body.', - example: 'Hello world', - }, - { - name: 'request.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Total size in bytes of the request (body and headers).', - example: 1437, - }, - { - name: 'request.method', - level: 'extended', - type: 'keyword', - description: - 'HTTP request method.\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'get, post, put', - }, - { - name: 'request.referrer', - level: 'extended', - type: 'keyword', - description: 'Referrer for this HTTP request.', - example: 'https://blog.example.com/', - }, - { - name: 'response.body.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Size in bytes of the response body.', - example: 887, - }, - { - name: 'response.body.content', - level: 'extended', - type: 'keyword', - description: 'The full HTTP response body.', - example: 'Hello world', - }, - { - name: 'response.bytes', - level: 'extended', - type: 'long', - format: 'bytes', - description: 'Total size in bytes of the response (body and headers).', - example: 1437, - }, - { - name: 'response.status_code', - level: 'extended', - type: 'long', - format: 'string', - description: 'HTTP response status code.', - example: 404, - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - description: 'HTTP version.', - example: 1.1, - }, - ], - }, - { - name: 'log', - title: 'Log', - group: 2, - description: 'Fields which are specific to log events.', - type: 'group', - fields: [ - { - name: 'level', - level: 'core', - type: 'keyword', - description: - 'Original log level of the log event.\n\nSome examples are `warn`, `error`, `i`.', - example: 'err', - }, - { - name: 'original', - level: 'core', - type: 'keyword', - description: - 'This is the original log message and contains the full log message\nbefore splitting it up in multiple parts.\n\nIn contrast to the `message` field which can contain an extracted part of\nthe log message, this field contains the original, full log message. It can\nhave already some modifications applied like encoding or new lines removed\nto clean up the log message.\n\nThis field is not indexed and doc_values are disabled so it cant be queried\nbut the value can be retrieved from `_source`.', - example: 'Sep 19 08:26:10 localhost My log', - }, - ], - }, - { - name: 'network', - title: 'Network', - group: 2, - description: - 'The network is defined as the communication path over which a host\nor network event happens.\n\nThe network.* fields should be populated with details about the network activity\nassociated with an event.', - type: 'group', - fields: [ - { - name: 'application', - level: 'extended', - type: 'keyword', - description: - 'A name given to an application level protocol. This can be arbitrarily\nassigned for things like microservices, but also apply to things like skype,\nicq, facebook, twitter. This would be used in situations where the vendor\nor service can be decoded such as from the source/dest IP owners, ports, or\nwire format.\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'aim', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: - 'Total bytes transferred in both directions.\n\nIf `source.bytes` and `destination.bytes` are known, `network.bytes` is their\nsum.', - example: 368, - }, - { - name: 'community_id', - level: 'extended', - type: 'keyword', - description: - 'A hash of source and destination IPs and ports, as well as the\nprotocol used in a communication. This is a tool-agnostic standard to identify\nflows.\n\nLearn more at https://github.com/corelight/community-id-spec.', - example: '1:hO+sN4H+MG5MY/8hIrXPqc4ZQz0=', - }, - { - name: 'direction', - level: 'core', - type: 'keyword', - description: - "Direction of the network traffic. Recommended values are: * inbound * outbound * internal * external * unknown When mapping events from a host-based monitoring context, populate this field from the host's point of view. When mapping events from a network or perimeter-based monitoring context, populate this field from the point of view of your network perimeter.", - example: 'inbound', - }, - { - name: 'forwarded_ip', - level: 'core', - type: 'ip', - description: 'Host IP address when the source IP address is the proxy.', - example: '192.1.1.2', - }, - { - name: 'iana_number', - level: 'extended', - type: 'keyword', - description: - 'IANA Protocol Number (https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml).\nStandardized list of protocols. This aligns well with NetFlow and sFlow related\nlogs which use the IANA Protocol Number.', - example: 6, - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - description: 'Name given by operators to sections of their network.', - example: 'Guest Wifi', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: - 'Total packets transferred in both directions.\n\nIf `source.packets` and `destination.packets` are known, `network.packets`\nis their sum.', - example: 24, - }, - { - name: 'protocol', - level: 'core', - type: 'keyword', - description: - 'L7 Network protocol name. ex. http, lumberjack, transport protocol.\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'http', - }, - { - name: 'transport', - level: 'core', - type: 'keyword', - description: - 'Same as network.iana_number, but instead using the Keyword name\nof the transport layer (udp, tcp, ipv6-icmp, etc.)\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'tcp', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - description: - 'In the OSI Model this would be the Network Layer. ipv4, ipv6,\nipsec, pim, etc\n\nThe field value must be normalized to lowercase for querying. See the documentation\nsection "Implementing ECS".', - example: 'ipv4', - }, - ], - }, - { - name: 'observer', - title: 'Observer', - group: 2, - description: - 'An observer is defined as a special network, security, or application\ndevice used to detect, observe, or create network, security, or application-related\nevents and metrics.\n\nThis could be a custom hardware appliance or a server that has been configured\nto run special network, security, or application software. Examples include\nfirewalls, intrusion detection/prevention systems, network monitoring sensors,\nweb application firewalls, data loss prevention systems, and APM servers. The\nobserver.* fields shall be populated with details of the system, if any, that\ndetects, observes and/or creates a network, security, or application event or\nmetric. Message queues and ETL components used in processing events or metrics\nare not considered observers in ECS.', - type: 'group', - fields: [ - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'hostname', - level: 'core', - type: 'keyword', - description: 'Hostname of the observer.', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: 'IP address of the observer.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - description: 'MAC address of the observer', - }, - { - name: 'os.family', - level: 'extended', - type: 'keyword', - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'os.full', - level: 'extended', - type: 'keyword', - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'os.kernel', - level: 'extended', - type: 'keyword', - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'os.name', - level: 'extended', - type: 'keyword', - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'os.platform', - level: 'extended', - type: 'keyword', - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'os.version', - level: 'extended', - type: 'keyword', - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - { - name: 'serial_number', - level: 'extended', - type: 'keyword', - description: 'Observer serial number.', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - description: - 'The type of the observer the data is coming from.\n\nThere is no predefined list of observer types. Some examples are `forwarder`,\n`firewall`, `ids`, `ips`, `proxy`, `poller`, `sensor`, `APM server`.', - example: 'firewall', - }, - { - name: 'vendor', - level: 'core', - type: 'keyword', - description: 'observer vendor information.', - }, - { - name: 'version', - level: 'core', - type: 'keyword', - description: 'Observer version.', - }, - ], - }, - { - name: 'organization', - title: 'Organization', - group: 2, - description: - 'The organization fields enrich data with information about the company\nor entity the data is associated with.\n\nThese fields help you arrange or filter data stored in an index by one or multiple\norganizations.', - type: 'group', - fields: [ - { - name: 'id', - level: 'extended', - type: 'keyword', - description: 'Unique identifier for the organization.', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - description: 'Organization name.', - }, - ], - }, - { - name: 'os', - title: 'Operating System', - group: 2, - description: 'The OS fields contain information about the operating system.', - type: 'group', - fields: [ - { - name: 'family', - level: 'extended', - type: 'keyword', - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'full', - level: 'extended', - type: 'keyword', - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'kernel', - level: 'extended', - type: 'keyword', - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'platform', - level: 'extended', - type: 'keyword', - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - ], - }, - { - name: 'process', - title: 'Process', - group: 2, - description: - 'These fields contain information about a process.\n\nThese fields can help you correlate metrics information with a process id/name\nfrom a log message. The `process.pid` often stays in the metric itself and\nis copied to the global field for correlation.', - type: 'group', - fields: [ - { - name: 'args', - level: 'extended', - type: 'keyword', - description: - 'Array of process arguments.\n\nMay be filtered to protect sensitive information.', - example: ['ssh', '-l', 'user', '10.0.0.16'], - }, - { - name: 'executable', - level: 'extended', - type: 'keyword', - description: 'Absolute path to the process executable.', - example: '/usr/bin/ssh', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - description: 'Process name.\n\nSometimes called program name or similar.', - example: 'ssh', - }, - { - name: 'pid', - level: 'core', - type: 'long', - format: 'string', - description: 'Process id.', - example: 4242, - }, - { - name: 'ppid', - level: 'extended', - type: 'long', - format: 'string', - description: "Parent process' pid.", - example: 4241, - }, - { - name: 'start', - level: 'extended', - type: 'date', - description: 'The time the process started.', - example: '2016-05-23T08:05:34.853Z', - }, - { - name: 'thread.id', - level: 'extended', - type: 'long', - format: 'string', - description: 'Thread ID.', - example: 4242, - }, - { - name: 'title', - level: 'extended', - type: 'keyword', - description: - 'Process title.\n\nThe proctitle, some times the same as process name. Can also be different:\nfor example a browser setting its title to the web page currently opened.', - }, - { - name: 'working_directory', - level: 'extended', - type: 'keyword', - description: 'The working directory of the process.', - example: '/home/alice', - }, - ], - }, - { - name: 'related', - title: 'Related', - group: 2, - description: - 'This field set is meant to facilitate pivoting around a piece of\ndata.\n\nSome pieces of information can be seen in many places in an ECS event. To facilitate\nsearching for them, store an array of all seen values to their corresponding\nfield in `related.`.\n\nA concrete example is IP addresses, which can be under host, observer, source,\ndestination, client, server, and network.forwarded_ip. If you append all IPs\nto `related.ip`, you can then search for a given IP trivially, no matter where\nit appeared, by querying `related.ip:a.b.c.d`.', - type: 'group', - fields: [ - { - name: 'ip', - level: 'extended', - type: 'ip', - description: 'All of the IPs seen on your event.', - }, - ], - }, - { - name: 'server', - title: 'Server', - group: 2, - description: - 'A Server is defined as the responder in a network connection for\nevents regarding sessions, connections, or bidirectional flow records.\n\nFor TCP events, the server is the receiver of the initial SYN packet(s) of the\nTCP connection. For other protocols, the server is generally the responder in\nthe network transaction. Some systems actually use the term "responder" to refer\nthe server in TCP connections. The server fields describe details about the\nsystem acting as the server in the network event. Server fields are usually\npopulated in conjunction with client fields. Server fields are generally not\npopulated for packet-level events.\n\nClient / server representations can add semantic context to an exchange, which\nis helpful to visualize the data in certain situations. If your context falls\nin that category, you should still ensure that source and destination are filled\nappropriately.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - description: - 'Some event server addresses are defined ambiguously. The event\nwill sometimes list an IP, a domain or a unix socket. You should always store\nthe raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the server to the client.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - description: 'Server domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: - 'IP address of the server.\n\nCan be one or multiple IPv4 or IPv6 addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - description: 'MAC address of the server.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the server to the client.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the server.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - description: 'One or multiple unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'service', - title: 'Service', - group: 2, - description: - 'The service fields describe the service for or from which the data\nwas collected.\n\nThese fields help you find and correlate logs for a specific service and version.', - type: 'group', - fields: [ - { - name: 'ephemeral_id', - level: 'extended', - type: 'keyword', - description: - 'Ephemeral identifier of this service (if one exists).\n\nThis id normally changes across restarts, but `service.id` does not.', - example: '8a4f500f', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - description: - 'Unique identifier of the running service.\n\nThis id should uniquely identify this service. This makes it possible to correlate\nlogs and metrics for one specific service.\n\nExample: If you are experiencing issues with one redis instance, you can filter\non that id to see metrics and logs for that single instance.', - example: 'd37e5ebfe0ae6c4972dbe9f0174a1637bb8247f6', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - description: - 'Name of the service data is collected from.\n\nThe name of the service is normally user given. This allows if two instances\nof the same service are running on the same machine they can be differentiated\nby the `service.name`.\n\nAlso it allows for distributed services that run on multiple hosts to correlate\nthe related instances based on the name.\n\nIn the case of Elasticsearch the service.name could contain the cluster name.\nFor Beats the service.name is by default a copy of the `service.type` field\nif no name is specified.', - example: 'elasticsearch-metrics', - }, - { - name: 'state', - level: 'core', - type: 'keyword', - description: 'Current state of the service.', - }, - { - name: 'type', - level: 'core', - type: 'keyword', - description: - 'The type of the service data is collected from.\n\nThe type can be used to group and correlate logs and metrics from one service\ntype.\n\nExample: If logs or metrics are collected from Elasticsearch, `service.type`\nwould be `elasticsearch`.', - example: 'elasticsearch', - }, - { - name: 'version', - level: 'core', - type: 'keyword', - description: - 'Version of the service the data was collected from.\n\nThis allows to look at a data set only for a specific version of a service.', - example: '3.2.4', - }, - ], - }, - { - name: 'source', - title: 'Source', - group: 2, - description: - 'Source fields describe details about the source of a packet/event.\n\nSource fields are usually populated in conjunction with destination fields.', - type: 'group', - fields: [ - { - name: 'address', - level: 'extended', - type: 'keyword', - description: - 'Some event source addresses are defined ambiguously. The event\nwill sometimes list an IP, a domain or a unix socket. You should always store\nthe raw address in the `.address` field.\n\nThen it should be duplicated to `.ip` or `.domain`, depending on which one\nit is.', - }, - { - name: 'bytes', - level: 'core', - type: 'long', - format: 'bytes', - description: 'Bytes sent from the source to the destination.', - example: 184, - }, - { - name: 'domain', - level: 'core', - type: 'keyword', - description: 'Source domain.', - }, - { - name: 'geo.city_name', - level: 'core', - type: 'keyword', - description: 'City name.', - example: 'Montreal', - }, - { - name: 'geo.continent_name', - level: 'core', - type: 'keyword', - description: 'Name of the continent.', - example: 'North America', - }, - { - name: 'geo.country_iso_code', - level: 'core', - type: 'keyword', - description: 'Country ISO code.', - example: 'CA', - }, - { - name: 'geo.country_name', - level: 'core', - type: 'keyword', - description: 'Country name.', - example: 'Canada', - }, - { - name: 'geo.location', - level: 'core', - type: 'geo_point', - description: 'Longitude and latitude.', - example: '{ "lon": -73.614830, "lat": 45.505918 }', - }, - { - name: 'geo.name', - level: 'extended', - type: 'keyword', - description: - 'User-defined description of a location, at the level of granularity\nthey care about.\n\nCould be the name of their data centers, the floor number, if this describes\na local physical entity, city names.\n\nNot typically used in automated geolocation.', - example: 'boston-dc', - }, - { - name: 'geo.region_iso_code', - level: 'core', - type: 'keyword', - description: 'Region ISO code.', - example: 'CA-QC', - }, - { - name: 'geo.region_name', - level: 'core', - type: 'keyword', - description: 'Region name.', - example: 'Quebec', - }, - { - name: 'ip', - level: 'core', - type: 'ip', - description: - 'IP address of the source.\n\nCan be one or multiple IPv4 or IPv6 addresses.', - }, - { - name: 'mac', - level: 'core', - type: 'keyword', - description: 'MAC address of the source.', - }, - { - name: 'packets', - level: 'core', - type: 'long', - description: 'Packets sent from the source to the destination.', - example: 12, - }, - { - name: 'port', - level: 'core', - type: 'long', - format: 'string', - description: 'Port of the source.', - }, - { - name: 'user.email', - level: 'extended', - type: 'keyword', - description: 'User email address.', - }, - { - name: 'user.full_name', - level: 'extended', - type: 'keyword', - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'user.group.id', - level: 'extended', - type: 'keyword', - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'user.group.name', - level: 'extended', - type: 'keyword', - description: 'Name of the group.', - }, - { - name: 'user.hash', - level: 'extended', - type: 'keyword', - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'user.id', - level: 'core', - type: 'keyword', - description: 'One or multiple unique identifiers of the user.', - }, - { - name: 'user.name', - level: 'core', - type: 'keyword', - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'url', - title: 'URL', - group: 2, - description: - 'URL fields provide support for complete or partial URLs, and supports\nthe breaking down into scheme, domain, path, and so on.', - type: 'group', - fields: [ - { - name: 'domain', - level: 'extended', - type: 'keyword', - description: - 'Domain of the url, such as "www.elastic.co".\n\nIn some cases a URL may refer to an IP and/or port directly, without a domain\nname. In this case, the IP address would go to the `domain` field.', - example: 'www.elastic.co', - }, - { - name: 'fragment', - level: 'extended', - type: 'keyword', - description: - 'Portion of the url after the `#`, such as "top".\n\nThe `#` is not part of the fragment.', - }, - { - name: 'full', - level: 'extended', - type: 'keyword', - description: - 'If full URLs are important to your use case, they should be stored\nin `url.full`, whether this field is reconstructed or present in the event\nsource.', - example: 'https://www.elastic.co:443/search?q=elasticsearch#top', - }, - { - name: 'original', - level: 'extended', - type: 'keyword', - description: - 'Unmodified original url as seen in the event source.\n\nNote that in network monitoring, the observed URL may be a full URL, whereas\nin access logs, the URL is often just represented as a path.\n\nThis field is meant to represent the URL as it was observed, complete or not.', - example: - 'https://www.elastic.co:443/search?q=elasticsearch#top or /search?q=elasticsearch', - }, - { - name: 'password', - level: 'extended', - type: 'keyword', - description: 'Password of the request.', - }, - { - name: 'path', - level: 'extended', - type: 'keyword', - description: 'Path of the request, such as "/search".', - }, - { - name: 'port', - level: 'extended', - type: 'long', - format: 'string', - description: 'Port of the request, such as 443.', - example: 443, - }, - { - name: 'query', - level: 'extended', - type: 'keyword', - description: - 'The query field describes the query string of the request, such\nas "q=elasticsearch".\n\nThe `?` is excluded from the query string. If a URL contains no `?`, there\nis no query field. If there is a `?` but no query, the query field exists\nwith an empty string. The `exists` query can be used to differentiate between\nthe two cases.', - }, - { - name: 'scheme', - level: 'extended', - type: 'keyword', - description: - 'Scheme of the request, such as "https".\n\nNote: The `:` is not part of the scheme.', - example: 'https', - }, - { - name: 'username', - level: 'extended', - type: 'keyword', - description: 'Username of the request.', - }, - ], - }, - { - name: 'user', - title: 'User', - group: 2, - description: - 'The user fields describe information about the user that is relevant\nto the event.\n\nFields can have one entry or multiple entries. If a user has more than one id,\nprovide an array that includes all of them.', - type: 'group', - fields: [ - { - name: 'email', - level: 'extended', - type: 'keyword', - description: 'User email address.', - }, - { - name: 'full_name', - level: 'extended', - type: 'keyword', - description: "User's full name, if available.", - example: 'Albert Einstein', - }, - { - name: 'group.id', - level: 'extended', - type: 'keyword', - description: 'Unique identifier for the group on the system/platform.', - }, - { - name: 'group.name', - level: 'extended', - type: 'keyword', - description: 'Name of the group.', - }, - { - name: 'hash', - level: 'extended', - type: 'keyword', - description: - 'Unique user hash to correlate information for a user in anonymized\nform.\n\nUseful if `user.id` or `user.name` contain confidential information and cannot\nbe used.', - }, - { - name: 'id', - level: 'core', - type: 'keyword', - description: 'One or multiple unique identifiers of the user.', - }, - { - name: 'name', - level: 'core', - type: 'keyword', - description: 'Short name or login of the user.', - example: 'albert', - }, - ], - }, - { - name: 'user_agent', - title: 'User agent', - group: 2, - description: - 'The user_agent fields normally come from a browser request.\n\nThey often show up in web service logs coming from the parsed user agent string.', - type: 'group', - fields: [ - { - name: 'device.name', - level: 'extended', - type: 'keyword', - description: 'Name of the device.', - example: 'iPhone', - }, - { - name: 'name', - level: 'extended', - type: 'keyword', - description: 'Name of the user agent.', - example: 'Safari', - }, - { - name: 'original', - level: 'extended', - type: 'keyword', - description: 'Unparsed version of the user_agent.', - example: - 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_1 like Mac OS X) AppleWebKit/605.1.15\n(KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1', - }, - { - name: 'os.family', - level: 'extended', - type: 'keyword', - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - }, - { - name: 'os.full', - level: 'extended', - type: 'keyword', - description: 'Operating system name, including the version or code name.', - example: 'Mac OS Mojave', - }, - { - name: 'os.kernel', - level: 'extended', - type: 'keyword', - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - }, - { - name: 'os.name', - level: 'extended', - type: 'keyword', - description: 'Operating system name, without the version.', - example: 'Mac OS X', - }, - { - name: 'os.platform', - level: 'extended', - type: 'keyword', - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - }, - { - name: 'os.version', - level: 'extended', - type: 'keyword', - description: 'Operating system version as a raw string.', - example: '10.14.1', - }, - { - name: 'version', - level: 'extended', - type: 'keyword', - description: 'Version of the user agent.', - example: 12, - }, - ], - }, - ], - }, - { - key: 'beat', - title: 'Beat', - description: 'Contains common beat fields available in all event types.\n', - fields: [ - { - name: 'agent.hostname', - type: 'keyword', - description: 'Hostname of the agent.', - }, - { - name: 'beat.timezone', - type: 'alias', - path: 'event.timezone', - migration: true, - }, - { - name: 'fields', - type: 'object', - object_type: 'keyword', - description: 'Contains user configurable fields.\n', - }, - { - name: 'error', - type: 'group', - description: 'Error fields containing additional info in case of errors.\n', - fields: [ - { - name: 'type', - type: 'keyword', - description: 'Error type.\n', - }, - ], - }, - { - name: 'beat.name', - type: 'alias', - path: 'host.name', - migration: true, - }, - { - name: 'beat.hostname', - type: 'alias', - path: 'agent.hostname', - migration: true, - }, - { - name: 'timeseries.instance', - type: 'keyword', - description: 'Time series instance id', - }, - ], - }, - { - key: 'cloud', - title: 'Cloud provider metadata', - description: 'Metadata from cloud providers added by the add_cloud_metadata processor.\n', - fields: [ - { - name: 'cloud.project.id', - example: 'project-x', - description: 'Name of the project in Google Cloud.\n', - }, - { - name: 'meta.cloud.provider', - type: 'alias', - path: 'cloud.provider', - migration: true, - }, - { - name: 'meta.cloud.instance_id', - type: 'alias', - path: 'cloud.instance.id', - migration: true, - }, - { - name: 'meta.cloud.instance_name', - type: 'alias', - path: 'cloud.instance.name', - migration: true, - }, - { - name: 'meta.cloud.machine_type', - type: 'alias', - path: 'cloud.machine.type', - migration: true, - }, - { - name: 'meta.cloud.availability_zone', - type: 'alias', - path: 'cloud.availability_zone', - migration: true, - }, - { - name: 'meta.cloud.project_id', - type: 'alias', - path: 'cloud.project.id', - migration: true, - }, - { - name: 'meta.cloud.region', - type: 'alias', - path: 'cloud.region', - migration: true, - }, - ], - }, - { - key: 'docker', - title: 'Docker', - description: 'Docker stats collected from Docker.\n', - short_config: false, - anchor: 'docker-processor', - fields: [ - { - name: 'docker', - type: 'group', - fields: [ - { - name: 'container.id', - type: 'alias', - path: 'container.id', - migration: true, - }, - { - name: 'container.image', - type: 'alias', - path: 'container.image.name', - migration: true, - }, - { - name: 'container.name', - type: 'alias', - path: 'container.name', - migration: true, - }, - { - name: 'container.labels', - type: 'object', - object_type: 'keyword', - description: 'Image labels.\n', - }, - ], - }, - ], - }, - { - key: 'host', - title: 'Host', - description: 'Info collected for the host machine.\n', - anchor: 'host-processor', - fields: [ - { - name: 'host', - type: 'group', - fields: [ - { - name: 'containerized', - type: 'boolean', - description: 'If the host is a container.\n', - }, - { - name: 'os.build', - type: 'keyword', - example: '18D109', - description: 'OS build information.\n', - }, - { - name: 'os.codename', - type: 'keyword', - example: 'stretch', - description: 'OS codename, if any.\n', - }, - ], - }, - ], - }, - { - key: 'kubernetes', - title: 'Kubernetes', - description: 'Kubernetes metadata added by the kubernetes processor\n', - short_config: false, - anchor: 'kubernetes-processor', - fields: [ - { - name: 'kubernetes', - type: 'group', - fields: [ - { - name: 'pod.name', - type: 'keyword', - description: 'Kubernetes pod name\n', - }, - { - name: 'pod.uid', - type: 'keyword', - description: 'Kubernetes Pod UID\n', - }, - { - name: 'namespace', - type: 'keyword', - description: 'Kubernetes namespace\n', - }, - { - name: 'node.name', - type: 'keyword', - description: 'Kubernetes node name\n', - }, - { - name: 'labels', - type: 'object', - description: 'Kubernetes labels map\n', - }, - { - name: 'annotations', - type: 'object', - description: 'Kubernetes annotations map\n', - }, - { - name: 'replicaset.name', - type: 'keyword', - description: 'Kubernetes replicaset name\n', - }, - { - name: 'deployment.name', - type: 'keyword', - description: 'Kubernetes deployment name\n', - }, - { - name: 'statefulset.name', - type: 'keyword', - description: 'Kubernetes statefulset name\n', - }, - { - name: 'container.name', - type: 'keyword', - description: 'Kubernetes container name\n', - }, - { - name: 'container.image', - type: 'keyword', - description: 'Kubernetes container image\n', - }, - ], - }, - ], - }, - { - key: 'process', - title: 'Process', - description: 'Process metadata fields\n', - fields: [ - { - name: 'process', - type: 'group', - fields: [ - { - name: 'exe', - type: 'alias', - path: 'process.executable', - migration: true, - }, - ], - }, - ], - }, - { - key: 'jolokia-autodiscover', - title: 'Jolokia Discovery autodiscover provider', - description: 'Metadata from Jolokia Discovery added by the jolokia provider.\n', - fields: [ - { - name: 'jolokia.agent.version', - type: 'keyword', - description: 'Version number of jolokia agent.\n', - }, - { - name: 'jolokia.agent.id', - type: 'keyword', - description: - 'Each agent has a unique id which can be either provided during startup of the agent in form of a configuration parameter or being autodetected. If autodected, the id has several parts: The IP, the process id, hashcode of the agent and its type.\n', - }, - { - name: 'jolokia.server.product', - type: 'keyword', - description: 'The container product if detected.\n', - }, - { - name: 'jolokia.server.version', - type: 'keyword', - description: "The container's version (if detected).\n", - }, - { - name: 'jolokia.server.vendor', - type: 'keyword', - description: 'The vendor of the container the agent is running in.\n', - }, - { - name: 'jolokia.url', - type: 'keyword', - description: 'The URL how this agent can be contacted.\n', - }, - { - name: 'jolokia.secured', - type: 'boolean', - description: 'Whether the agent was configured for authentication or not.\n', - }, - ], - }, - { - key: 'winlog', - title: 'Windows Event Log fields emitted by Winlogbeat', - description: 'Fields from the Windows Event Log.\n', - fields: [ - { - name: 'log.file.path', - type: 'keyword', - required: false, - description: - 'The name of the file the event was read from when Winlogbeat is reading directly from an .evtx file.\n', - }, - { - name: 'event.code', - type: 'keyword', - required: false, - description: 'The code for this log message (Windows event ID).\n', - }, - { - name: 'event.original', - description: - 'The raw XML representation of the event obtained from Windows. This field is only available on operating systems supporting the Windows Event Log API (Microsoft Windows Vista and newer). This field is not included by default and must be enabled by setting `include_xml: true` as a configuration option for an individual event log.\nThe XML representation of the event is useful for troubleshooting purposes. The data in the fields reported by Winlogbeat can be compared to the data in the XML to diagnose problems.\n', - }, - { - name: 'winlog', - type: 'group', - description: 'All fields specific to the Windows Event Log are defined here.\n', - fields: [ - { - name: 'api', - required: true, - description: - 'The event log API type used to read the record. The possible values are "wineventlog" for the Windows Event Log API or "eventlogging" for the Event Logging API.\nThe Event Logging API was designed for Windows Server 2003 or Windows 2000 operating systems. In Windows Vista, the event logging infrastructure was redesigned. On Windows Vista or later operating systems, the Windows Event Log API is used. Winlogbeat automatically detects which API to use for reading event logs.\n', - }, - { - name: 'activity_id', - type: 'keyword', - required: false, - description: - 'A globally unique identifier that identifies the current activity. The events that are published with this identifier are part of the same activity.\n', - }, - { - name: 'computer_name', - type: 'keyword', - required: true, - description: - 'The name of the computer that generated the record. When using Windows event forwarding, this name can differ from `agent.hostname`.\n', - }, - { - name: 'event_data', - type: 'object', - object_type: 'keyword', - required: false, - description: - 'The event-specific data. This field is mutually exclusive with `user_data`. If you are capturing event data on versions prior to Windows Vista, the parameters in `event_data` are named `param1`, `param2`, and so on, because event log parameters are unnamed in earlier versions of Windows.\n', - }, - { - name: 'event_id', - type: 'keyword', - required: true, - description: - 'The event identifier. The value is specific to the source of the event.\n', - }, - { - name: 'keywords', - type: 'keyword', - required: false, - description: 'The keywords are used to classify an event.\n', - }, - { - name: 'channel', - type: 'keyword', - required: true, - description: - 'The name of the channel from which this record was read. This value is one of the names from the `event_logs` collection in the configuration.\n', - }, - { - name: 'record_id', - type: 'keyword', - required: true, - description: - 'The record ID of the event log record. The first record written to an event log is record number 1, and other records are numbered sequentially. If the record number reaches the maximum value (2^32^ for the Event Logging API and 2^64^ for the Windows Event Log API), the next record number will be 0.\n', - }, - { - name: 'related_activity_id', - type: 'keyword', - required: false, - description: - 'A globally unique identifier that identifies the activity to which control was transferred to. The related events would then have this identifier as their `activity_id` identifier.\n', - }, - { - name: 'opcode', - type: 'keyword', - required: false, - description: - 'The opcode defined in the event. Task and opcode are typically used to identify the location in the application from where the event was logged.\n', - }, - { - name: 'provider_guid', - type: 'keyword', - required: false, - description: - 'A globally unique identifier that identifies the provider that logged the event.\n', - }, - { - name: 'process.pid', - type: 'long', - required: false, - description: 'The process_id of the Client Server Runtime Process.\n', - }, - { - name: 'provider_name', - type: 'keyword', - required: true, - description: - 'The source of the event log record (the application or service that logged the record).\n', - }, - { - name: 'task', - type: 'keyword', - required: false, - description: - 'The task defined in the event. Task and opcode are typically used to identify the location in the application from where the event was logged. The category used by the Event Logging API (on pre Windows Vista operating systems) is written to this field.\n', - }, - { - name: 'process.thread.id', - type: 'long', - required: false, - }, - { - name: 'user_data', - type: 'object', - object_type: 'keyword', - required: false, - description: - 'The event specific data. This field is mutually exclusive with `event_data`.\n', - }, - { - name: 'user.identifier', - type: 'keyword', - required: false, - example: 'S-1-5-21-3541430928-2051711210-1391384369-1001', - description: - 'The Windows security identifier (SID) of the account associated with this event.\n\nIf Winlogbeat cannot resolve the SID to a name, then the `user.name`, `user.domain`, and `user.type` fields will be omitted from the event. If you discover Winlogbeat not resolving SIDs, review the log for clues as to what the problem may be.\n', - }, - { - name: 'user.domain', - type: 'keyword', - required: false, - description: 'The domain that the account associated with this event is a member of.\n', - }, - { - name: 'user.type', - type: 'keyword', - required: false, - description: 'The type of account associated with this event.\n', - }, - { - name: 'version', - type: 'long', - required: false, - description: "The version number of the event's definition.", - }, - ], - }, - ], - }, - { - key: 'eventlog', - title: 'Event log record', - description: 'Contains data from a Windows event log record.\n', - fields: [ - { - name: 'type', - type: 'alias', - path: 'winlog.api', - migration: true, - }, - { - name: 'activity_id', - type: 'alias', - path: 'winlog.activity_id', - migration: true, - }, - { - name: 'computer_name', - type: 'alias', - path: 'winlog.computer_name', - migration: true, - }, - { - name: 'event_id', - type: 'alias', - path: 'winlog.event_id', - migration: true, - }, - { - name: 'keywords', - type: 'alias', - path: 'winlog.keywords', - migration: true, - }, - { - name: 'log_name', - type: 'alias', - path: 'winlog.channel', - migration: true, - }, - { - name: 'message_error', - type: 'alias', - path: 'error.message', - migration: true, - }, - { - name: 'record_number', - type: 'alias', - path: 'winlog.record_id', - migration: true, - }, - { - name: 'related_activity_id', - type: 'alias', - path: 'winlog.related_activity_id', - migration: true, - }, - { - name: 'opcode', - type: 'alias', - path: 'winlog.opcode', - migration: true, - }, - { - name: 'provider_guid', - type: 'alias', - path: 'winlog.provider_guid', - migration: true, - }, - { - name: 'process_id', - type: 'alias', - path: 'winlog.process.pid', - migration: true, - }, - { - name: 'source_name', - type: 'alias', - path: 'winlog.provider_name', - migration: true, - }, - { - name: 'task', - type: 'alias', - path: 'winlog.task', - migration: true, - }, - { - name: 'thread_id', - type: 'alias', - path: 'winlog.process.thread.id', - migration: true, - }, - { - name: 'user.identifier', - type: 'alias', - path: 'winlog.user.identifier', - migration: true, - }, - { - name: 'user.domain', - type: 'alias', - path: 'winlog.user.domain', - migration: true, - }, - { - name: 'user.type', - type: 'alias', - path: 'winlog.user.type', - migration: true, - }, - { - name: 'version', - type: 'alias', - path: 'winlog.version', - migration: true, - }, - { - name: 'xml', - type: 'alias', - path: 'event.original', - migration: true, - }, - ], - }, -]; diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/fields.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/fields.ts new file mode 100644 index 0000000000000..e61b9ae008a62 --- /dev/null +++ b/x-pack/plugins/security_solution/server/utils/beat_schema/fields.ts @@ -0,0 +1,36118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { BeatFields } from '../../../common/search_strategy/index_fields'; + +/* eslint-disable @typescript-eslint/naming-convention */ +export const fieldsBeat: BeatFields = { + _id: { + category: 'base', + description: 'Each document has an _id that uniquely identifies it', + example: 'Y-6TfmcB0WOhS6qyMv3s', + name: '_id', + type: 'keyword', + }, + _index: { + category: 'base', + description: + 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', + example: 'auditbeat-8.0.0-2019.02.19-000001', + name: '_index', + type: 'keyword', + }, + '@timestamp': { + category: 'base', + description: + 'Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + name: '@timestamp', + type: 'date', + }, + labels: { + category: 'base', + description: + 'Custom key/value pairs. Can be used to add meta information to events. Should not contain nested objects. All values are stored as keyword. Example: `docker` and `k8s` labels.', + example: '{"application": "foo-bar", "env": "production"}', + name: 'labels', + type: 'object', + }, + message: { + category: 'base', + description: + 'For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.', + example: 'Hello World', + name: 'message', + type: 'text', + }, + tags: { + category: 'base', + description: 'List of keywords used to tag each event.', + example: '["production", "env2"]', + name: 'tags', + type: 'keyword', + }, + 'agent.ephemeral_id': { + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + name: 'agent.ephemeral_id', + type: 'keyword', + }, + 'agent.id': { + category: 'agent', + description: + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', + example: '8a4f500d', + name: 'agent.id', + type: 'keyword', + }, + 'agent.name': { + category: 'agent', + description: + 'Custom name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', + example: 'foo', + name: 'agent.name', + type: 'keyword', + }, + 'agent.type': { + category: 'agent', + description: + 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', + example: 'filebeat', + name: 'agent.type', + type: 'keyword', + }, + 'agent.version': { + category: 'agent', + description: 'Version of the agent.', + example: '6.0.0-rc2', + name: 'agent.version', + type: 'keyword', + }, + 'as.number': { + category: 'as', + description: + 'Unique number allocated to the autonomous system. The autonomous system number (ASN) uniquely identifies each network on the Internet.', + example: 15169, + name: 'as.number', + type: 'long', + }, + 'as.organization.name': { + category: 'as', + description: 'Organization name.', + example: 'Google LLC', + name: 'as.organization.name', + type: 'keyword', + }, + 'client.address': { + category: 'client', + description: + 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + name: 'client.address', + type: 'keyword', + }, + 'client.as.number': { + category: 'client', + description: + 'Unique number allocated to the autonomous system. The autonomous system number (ASN) uniquely identifies each network on the Internet.', + example: 15169, + name: 'client.as.number', + type: 'long', + }, + 'client.as.organization.name': { + category: 'client', + description: 'Organization name.', + example: 'Google LLC', + name: 'client.as.organization.name', + type: 'keyword', + }, + 'client.bytes': { + category: 'client', + description: 'Bytes sent from the client to the server.', + example: 184, + name: 'client.bytes', + type: 'long', + format: 'bytes', + }, + 'client.domain': { + category: 'client', + description: 'Client domain.', + name: 'client.domain', + type: 'keyword', + }, + 'client.geo.city_name': { + category: 'client', + description: 'City name.', + example: 'Montreal', + name: 'client.geo.city_name', + type: 'keyword', + }, + 'client.geo.continent_name': { + category: 'client', + description: 'Name of the continent.', + example: 'North America', + name: 'client.geo.continent_name', + type: 'keyword', + }, + 'client.geo.country_iso_code': { + category: 'client', + description: 'Country ISO code.', + example: 'CA', + name: 'client.geo.country_iso_code', + type: 'keyword', + }, + 'client.geo.country_name': { + category: 'client', + description: 'Country name.', + example: 'Canada', + name: 'client.geo.country_name', + type: 'keyword', + }, + 'client.geo.location': { + category: 'client', + description: 'Longitude and latitude.', + example: '{ "lon": -73.614830, "lat": 45.505918 }', + name: 'client.geo.location', + type: 'geo_point', + }, + 'client.geo.name': { + category: 'client', + description: + 'User-defined description of a location, at the level of granularity they care about. Could be the name of their data centers, the floor number, if this describes a local physical entity, city names. Not typically used in automated geolocation.', + example: 'boston-dc', + name: 'client.geo.name', + type: 'keyword', + }, + 'client.geo.region_iso_code': { + category: 'client', + description: 'Region ISO code.', + example: 'CA-QC', + name: 'client.geo.region_iso_code', + type: 'keyword', + }, + 'client.geo.region_name': { + category: 'client', + description: 'Region name.', + example: 'Quebec', + name: 'client.geo.region_name', + type: 'keyword', + }, + 'client.ip': { + category: 'client', + description: 'IP address of the client. Can be one or multiple IPv4 or IPv6 addresses.', + name: 'client.ip', + type: 'ip', + }, + 'client.mac': { + category: 'client', + description: 'MAC address of the client.', + name: 'client.mac', + type: 'keyword', + }, + 'client.nat.ip': { + category: 'client', + description: + 'Translated IP of source based NAT sessions (e.g. internal client to internet). Typically connections traversing load balancers, firewalls, or routers.', + name: 'client.nat.ip', + type: 'ip', + }, + 'client.nat.port': { + category: 'client', + description: + 'Translated port of source based NAT sessions (e.g. internal client to internet). Typically connections traversing load balancers, firewalls, or routers.', + name: 'client.nat.port', + type: 'long', + format: 'string', + }, + 'client.packets': { + category: 'client', + description: 'Packets sent from the client to the server.', + example: 12, + name: 'client.packets', + type: 'long', + }, + 'client.port': { + category: 'client', + description: 'Port of the client.', + name: 'client.port', + type: 'long', + format: 'string', + }, + 'client.registered_domain': { + category: 'client', + description: + 'The highest registered client domain, stripped of the subdomain. For example, the registered domain for "foo.google.com" is "google.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', + example: 'google.com', + name: 'client.registered_domain', + type: 'keyword', + }, + 'client.top_level_domain': { + category: 'client', + description: + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for google.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', + example: 'co.uk', + name: 'client.top_level_domain', + type: 'keyword', + }, + 'client.user.domain': { + category: 'client', + description: + 'Name of the directory the user is a member of. For example, an LDAP or Active Directory domain name.', + name: 'client.user.domain', + type: 'keyword', + }, + 'client.user.email': { + category: 'client', + description: 'User email address.', + name: 'client.user.email', + type: 'keyword', + }, + 'client.user.full_name': { + category: 'client', + description: "User's full name, if available.", + example: 'Albert Einstein', + name: 'client.user.full_name', + type: 'keyword', + }, + 'client.user.group.domain': { + category: 'client', + description: + 'Name of the directory the group is a member of. For example, an LDAP or Active Directory domain name.', + name: 'client.user.group.domain', + type: 'keyword', + }, + 'client.user.group.id': { + category: 'client', + description: 'Unique identifier for the group on the system/platform.', + name: 'client.user.group.id', + type: 'keyword', + }, + 'client.user.group.name': { + category: 'client', + description: 'Name of the group.', + name: 'client.user.group.name', + type: 'keyword', + }, + 'client.user.hash': { + category: 'client', + description: + 'Unique user hash to correlate information for a user in anonymized form. Useful if `user.id` or `user.name` contain confidential information and cannot be used.', + name: 'client.user.hash', + type: 'keyword', + }, + 'client.user.id': { + category: 'client', + description: 'Unique identifiers of the user.', + name: 'client.user.id', + type: 'keyword', + }, + 'client.user.name': { + category: 'client', + description: 'Short name or login of the user.', + example: 'albert', + name: 'client.user.name', + type: 'keyword', + }, + 'cloud.account.id': { + category: 'cloud', + description: + 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', + example: 666777888999, + name: 'cloud.account.id', + type: 'keyword', + }, + 'cloud.availability_zone': { + category: 'cloud', + description: 'Availability zone in which this host is running.', + example: 'us-east-1c', + name: 'cloud.availability_zone', + type: 'keyword', + }, + 'cloud.instance.id': { + category: 'cloud', + description: 'Instance ID of the host machine.', + example: 'i-1234567890abcdef0', + name: 'cloud.instance.id', + type: 'keyword', + }, + 'cloud.instance.name': { + category: 'cloud', + description: 'Instance name of the host machine.', + name: 'cloud.instance.name', + type: 'keyword', + }, + 'cloud.machine.type': { + category: 'cloud', + description: 'Machine type of the host machine.', + example: 't2.medium', + name: 'cloud.machine.type', + type: 'keyword', + }, + 'cloud.provider': { + category: 'cloud', + description: 'Name of the cloud provider. Example values are aws, azure, gcp, or digitalocean.', + example: 'aws', + name: 'cloud.provider', + type: 'keyword', + }, + 'cloud.region': { + category: 'cloud', + description: 'Region in which this host is running.', + example: 'us-east-1', + name: 'cloud.region', + type: 'keyword', + }, + 'code_signature.exists': { + category: 'code_signature', + description: 'Boolean to capture if a signature is present.', + example: 'true', + name: 'code_signature.exists', + type: 'boolean', + }, + 'code_signature.status': { + category: 'code_signature', + description: + 'Additional information about the certificate status. This is useful for logging cryptographic errors with the certificate validity or trust status. Leave unpopulated if the validity or trust of the certificate was unchecked.', + example: 'ERROR_UNTRUSTED_ROOT', + name: 'code_signature.status', + type: 'keyword', + }, + 'code_signature.subject_name': { + category: 'code_signature', + description: 'Subject name of the code signer', + example: 'Microsoft Corporation', + name: 'code_signature.subject_name', + type: 'keyword', + }, + 'code_signature.trusted': { + category: 'code_signature', + description: + 'Stores the trust status of the certificate chain. Validating the trust of the certificate chain may be complicated, and this field should only be populated by tools that actively check the status.', + example: 'true', + name: 'code_signature.trusted', + type: 'boolean', + }, + 'code_signature.valid': { + category: 'code_signature', + description: + 'Boolean to capture if the digital signature is verified against the binary content. Leave unpopulated if a certificate was unchecked.', + example: 'true', + name: 'code_signature.valid', + type: 'boolean', + }, + 'container.id': { + category: 'container', + description: 'Unique container id.', + name: 'container.id', + type: 'keyword', + }, + 'container.image.name': { + category: 'container', + description: 'Name of the image the container was built on.', + name: 'container.image.name', + type: 'keyword', + }, + 'container.image.tag': { + category: 'container', + description: 'Container image tags.', + name: 'container.image.tag', + type: 'keyword', + }, + 'container.labels': { + category: 'container', + description: 'Image labels.', + name: 'container.labels', + type: 'object', + }, + 'container.name': { + category: 'container', + description: 'Container name.', + name: 'container.name', + type: 'keyword', + }, + 'container.runtime': { + category: 'container', + description: 'Runtime managing this container.', + example: 'docker', + name: 'container.runtime', + type: 'keyword', + }, + 'destination.address': { + category: 'destination', + description: + 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + name: 'destination.address', + type: 'keyword', + }, + 'destination.as.number': { + category: 'destination', + description: + 'Unique number allocated to the autonomous system. The autonomous system number (ASN) uniquely identifies each network on the Internet.', + example: 15169, + name: 'destination.as.number', + type: 'long', + }, + 'destination.as.organization.name': { + category: 'destination', + description: 'Organization name.', + example: 'Google LLC', + name: 'destination.as.organization.name', + type: 'keyword', + }, + 'destination.bytes': { + category: 'destination', + description: 'Bytes sent from the destination to the source.', + example: 184, + name: 'destination.bytes', + type: 'long', + format: 'bytes', + }, + 'destination.domain': { + category: 'destination', + description: 'Destination domain.', + name: 'destination.domain', + type: 'keyword', + }, + 'destination.geo.city_name': { + category: 'destination', + description: 'City name.', + example: 'Montreal', + name: 'destination.geo.city_name', + type: 'keyword', + }, + 'destination.geo.continent_name': { + category: 'destination', + description: 'Name of the continent.', + example: 'North America', + name: 'destination.geo.continent_name', + type: 'keyword', + }, + 'destination.geo.country_iso_code': { + category: 'destination', + description: 'Country ISO code.', + example: 'CA', + name: 'destination.geo.country_iso_code', + type: 'keyword', + }, + 'destination.geo.country_name': { + category: 'destination', + description: 'Country name.', + example: 'Canada', + name: 'destination.geo.country_name', + type: 'keyword', + }, + 'destination.geo.location': { + category: 'destination', + description: 'Longitude and latitude.', + example: '{ "lon": -73.614830, "lat": 45.505918 }', + name: 'destination.geo.location', + type: 'geo_point', + }, + 'destination.geo.name': { + category: 'destination', + description: + 'User-defined description of a location, at the level of granularity they care about. Could be the name of their data centers, the floor number, if this describes a local physical entity, city names. Not typically used in automated geolocation.', + example: 'boston-dc', + name: 'destination.geo.name', + type: 'keyword', + }, + 'destination.geo.region_iso_code': { + category: 'destination', + description: 'Region ISO code.', + example: 'CA-QC', + name: 'destination.geo.region_iso_code', + type: 'keyword', + }, + 'destination.geo.region_name': { + category: 'destination', + description: 'Region name.', + example: 'Quebec', + name: 'destination.geo.region_name', + type: 'keyword', + }, + 'destination.ip': { + category: 'destination', + description: 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', + name: 'destination.ip', + type: 'ip', + }, + 'destination.mac': { + category: 'destination', + description: 'MAC address of the destination.', + name: 'destination.mac', + type: 'keyword', + }, + 'destination.nat.ip': { + category: 'destination', + description: + 'Translated ip of destination based NAT sessions (e.g. internet to private DMZ) Typically used with load balancers, firewalls, or routers.', + name: 'destination.nat.ip', + type: 'ip', + }, + 'destination.nat.port': { + category: 'destination', + description: + 'Port the source session is translated to by NAT Device. Typically used with load balancers, firewalls, or routers.', + name: 'destination.nat.port', + type: 'long', + format: 'string', + }, + 'destination.packets': { + category: 'destination', + description: 'Packets sent from the destination to the source.', + example: 12, + name: 'destination.packets', + type: 'long', + }, + 'destination.port': { + category: 'destination', + description: 'Port of the destination.', + name: 'destination.port', + type: 'long', + format: 'string', + }, + 'destination.registered_domain': { + category: 'destination', + description: + 'The highest registered destination domain, stripped of the subdomain. For example, the registered domain for "foo.google.com" is "google.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', + example: 'google.com', + name: 'destination.registered_domain', + type: 'keyword', + }, + 'destination.top_level_domain': { + category: 'destination', + description: + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for google.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', + example: 'co.uk', + name: 'destination.top_level_domain', + type: 'keyword', + }, + 'destination.user.domain': { + category: 'destination', + description: + 'Name of the directory the user is a member of. For example, an LDAP or Active Directory domain name.', + name: 'destination.user.domain', + type: 'keyword', + }, + 'destination.user.email': { + category: 'destination', + description: 'User email address.', + name: 'destination.user.email', + type: 'keyword', + }, + 'destination.user.full_name': { + category: 'destination', + description: "User's full name, if available.", + example: 'Albert Einstein', + name: 'destination.user.full_name', + type: 'keyword', + }, + 'destination.user.group.domain': { + category: 'destination', + description: + 'Name of the directory the group is a member of. For example, an LDAP or Active Directory domain name.', + name: 'destination.user.group.domain', + type: 'keyword', + }, + 'destination.user.group.id': { + category: 'destination', + description: 'Unique identifier for the group on the system/platform.', + name: 'destination.user.group.id', + type: 'keyword', + }, + 'destination.user.group.name': { + category: 'destination', + description: 'Name of the group.', + name: 'destination.user.group.name', + type: 'keyword', + }, + 'destination.user.hash': { + category: 'destination', + description: + 'Unique user hash to correlate information for a user in anonymized form. Useful if `user.id` or `user.name` contain confidential information and cannot be used.', + name: 'destination.user.hash', + type: 'keyword', + }, + 'destination.user.id': { + category: 'destination', + description: 'Unique identifiers of the user.', + name: 'destination.user.id', + type: 'keyword', + }, + 'destination.user.name': { + category: 'destination', + description: 'Short name or login of the user.', + example: 'albert', + name: 'destination.user.name', + type: 'keyword', + }, + 'dll.code_signature.exists': { + category: 'dll', + description: 'Boolean to capture if a signature is present.', + example: 'true', + name: 'dll.code_signature.exists', + type: 'boolean', + }, + 'dll.code_signature.status': { + category: 'dll', + description: + 'Additional information about the certificate status. This is useful for logging cryptographic errors with the certificate validity or trust status. Leave unpopulated if the validity or trust of the certificate was unchecked.', + example: 'ERROR_UNTRUSTED_ROOT', + name: 'dll.code_signature.status', + type: 'keyword', + }, + 'dll.code_signature.subject_name': { + category: 'dll', + description: 'Subject name of the code signer', + example: 'Microsoft Corporation', + name: 'dll.code_signature.subject_name', + type: 'keyword', + }, + 'dll.code_signature.trusted': { + category: 'dll', + description: + 'Stores the trust status of the certificate chain. Validating the trust of the certificate chain may be complicated, and this field should only be populated by tools that actively check the status.', + example: 'true', + name: 'dll.code_signature.trusted', + type: 'boolean', + }, + 'dll.code_signature.valid': { + category: 'dll', + description: + 'Boolean to capture if the digital signature is verified against the binary content. Leave unpopulated if a certificate was unchecked.', + example: 'true', + name: 'dll.code_signature.valid', + type: 'boolean', + }, + 'dll.hash.md5': { + category: 'dll', + description: 'MD5 hash.', + name: 'dll.hash.md5', + type: 'keyword', + }, + 'dll.hash.sha1': { + category: 'dll', + description: 'SHA1 hash.', + name: 'dll.hash.sha1', + type: 'keyword', + }, + 'dll.hash.sha256': { + category: 'dll', + description: 'SHA256 hash.', + name: 'dll.hash.sha256', + type: 'keyword', + }, + 'dll.hash.sha512': { + category: 'dll', + description: 'SHA512 hash.', + name: 'dll.hash.sha512', + type: 'keyword', + }, + 'dll.name': { + category: 'dll', + description: 'Name of the library. This generally maps to the name of the file on disk.', + example: 'kernel32.dll', + name: 'dll.name', + type: 'keyword', + }, + 'dll.path': { + category: 'dll', + description: 'Full file path of the library.', + example: 'C:\\Windows\\System32\\kernel32.dll', + name: 'dll.path', + type: 'keyword', + }, + 'dll.pe.company': { + category: 'dll', + description: 'Internal company name of the file, provided at compile-time.', + example: 'Microsoft Corporation', + name: 'dll.pe.company', + type: 'keyword', + }, + 'dll.pe.description': { + category: 'dll', + description: 'Internal description of the file, provided at compile-time.', + example: 'Paint', + name: 'dll.pe.description', + type: 'keyword', + }, + 'dll.pe.file_version': { + category: 'dll', + description: 'Internal version of the file, provided at compile-time.', + example: '6.3.9600.17415', + name: 'dll.pe.file_version', + type: 'keyword', + }, + 'dll.pe.original_file_name': { + category: 'dll', + description: 'Internal name of the file, provided at compile-time.', + example: 'MSPAINT.EXE', + name: 'dll.pe.original_file_name', + type: 'keyword', + }, + 'dll.pe.product': { + category: 'dll', + description: 'Internal product name of the file, provided at compile-time.', + example: 'Microsoft® Windows® Operating System', + name: 'dll.pe.product', + type: 'keyword', + }, + 'dns.answers': { + category: 'dns', + description: + 'An array containing an object for each answer section returned by the server. The main keys that should be present in these objects are defined by ECS. Records that have more information may contain more keys than what ECS defines. Not all DNS data sources give all details about DNS answers. At minimum, answer objects must contain the `data` key. If more information is available, map as much of it to ECS as possible, and add any additional fields to the answer objects as custom fields.', + name: 'dns.answers', + type: 'object', + }, + 'dns.answers.class': { + category: 'dns', + description: 'The class of DNS data contained in this resource record.', + example: 'IN', + name: 'dns.answers.class', + type: 'keyword', + }, + 'dns.answers.data': { + category: 'dns', + description: + 'The data describing the resource. The meaning of this data depends on the type and class of the resource record.', + example: '10.10.10.10', + name: 'dns.answers.data', + type: 'keyword', + }, + 'dns.answers.name': { + category: 'dns', + description: + "The domain name to which this resource record pertains. If a chain of CNAME is being resolved, each answer's `name` should be the one that corresponds with the answer's `data`. It should not simply be the original `question.name` repeated.", + example: 'www.google.com', + name: 'dns.answers.name', + type: 'keyword', + }, + 'dns.answers.ttl': { + category: 'dns', + description: + 'The time interval in seconds that this resource record may be cached before it should be discarded. Zero values mean that the data should not be cached.', + example: 180, + name: 'dns.answers.ttl', + type: 'long', + }, + 'dns.answers.type': { + category: 'dns', + description: 'The type of data contained in this resource record.', + example: 'CNAME', + name: 'dns.answers.type', + type: 'keyword', + }, + 'dns.header_flags': { + category: 'dns', + description: + 'Array of 2 letter DNS header flags. Expected values are: AA, TC, RD, RA, AD, CD, DO.', + example: '["RD","RA"]', + name: 'dns.header_flags', + type: 'keyword', + }, + 'dns.id': { + category: 'dns', + description: + 'The DNS packet identifier assigned by the program that generated the query. The identifier is copied to the response.', + example: 62111, + name: 'dns.id', + type: 'keyword', + }, + 'dns.op_code': { + category: 'dns', + description: + 'The DNS operation code that specifies the kind of query in the message. This value is set by the originator of a query and copied into the response.', + example: 'QUERY', + name: 'dns.op_code', + type: 'keyword', + }, + 'dns.question.class': { + category: 'dns', + description: 'The class of records being queried.', + example: 'IN', + name: 'dns.question.class', + type: 'keyword', + }, + 'dns.question.name': { + category: 'dns', + description: + 'The name being queried. If the name field contains non-printable characters (below 32 or above 126), those characters should be represented as escaped base 10 integers (\\DDD). Back slashes and quotes should be escaped. Tabs, carriage returns, and line feeds should be converted to \\t, \\r, and \\n respectively.', + example: 'www.google.com', + name: 'dns.question.name', + type: 'keyword', + }, + 'dns.question.registered_domain': { + category: 'dns', + description: + 'The highest registered domain, stripped of the subdomain. For example, the registered domain for "foo.google.com" is "google.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', + example: 'google.com', + name: 'dns.question.registered_domain', + type: 'keyword', + }, + 'dns.question.subdomain': { + category: 'dns', + description: + 'The subdomain is all of the labels under the registered_domain. If the domain has multiple levels of subdomain, such as "sub2.sub1.example.com", the subdomain field should contain "sub2.sub1", with no trailing period.', + example: 'www', + name: 'dns.question.subdomain', + type: 'keyword', + }, + 'dns.question.top_level_domain': { + category: 'dns', + description: + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for google.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', + example: 'co.uk', + name: 'dns.question.top_level_domain', + type: 'keyword', + }, + 'dns.question.type': { + category: 'dns', + description: 'The type of record being queried.', + example: 'AAAA', + name: 'dns.question.type', + type: 'keyword', + }, + 'dns.resolved_ip': { + category: 'dns', + description: + 'Array containing all IPs seen in `answers.data`. The `answers` array can be difficult to use, because of the variety of data formats it can contain. Extracting all IP addresses seen in there to `dns.resolved_ip` makes it possible to index them as IP addresses, and makes them easier to visualize and query for.', + example: '["10.10.10.10","10.10.10.11"]', + name: 'dns.resolved_ip', + type: 'ip', + }, + 'dns.response_code': { + category: 'dns', + description: 'The DNS response code.', + example: 'NOERROR', + name: 'dns.response_code', + type: 'keyword', + }, + 'dns.type': { + category: 'dns', + description: + 'The type of DNS event captured, query or answer. If your source of DNS events only gives you DNS queries, you should only create dns events of type `dns.type:query`. If your source of DNS events gives you answers as well, you should create one event per query (optionally as soon as the query is seen). And a second event containing all query details as well as an array of answers.', + example: 'answer', + name: 'dns.type', + type: 'keyword', + }, + 'ecs.version': { + category: 'ecs', + description: + 'ECS version this event conforms to. `ecs.version` is a required field and must exist in all events. When querying across multiple indices -- which may conform to slightly different ECS versions -- this field lets integrations adjust to the schema version of the events.', + example: '1.0.0', + name: 'ecs.version', + type: 'keyword', + }, + 'error.code': { + category: 'error', + description: 'Error code describing the error.', + name: 'error.code', + type: 'keyword', + }, + 'error.id': { + category: 'error', + description: 'Unique identifier for the error.', + name: 'error.id', + type: 'keyword', + }, + 'error.message': { + category: 'error', + description: 'Error message.', + name: 'error.message', + type: 'text', + }, + 'error.stack_trace': { + category: 'error', + description: 'The stack trace of this error in plain text.', + name: 'error.stack_trace', + type: 'keyword', + }, + 'error.type': { + category: 'error', + description: 'The type of the error, for example the class name of the exception.', + example: 'java.lang.NullPointerException', + name: 'error.type', + type: 'keyword', + }, + 'event.action': { + category: 'event', + description: + 'The action captured by the event. This describes the information in the event. It is more specific than `event.category`. Examples are `group-add`, `process-started`, `file-created`. The value is normally defined by the implementer.', + example: 'user-password-change', + name: 'event.action', + type: 'keyword', + }, + 'event.category': { + category: 'event', + description: + 'This is one of four ECS Categorization Fields, and indicates the second level in the ECS category hierarchy. `event.category` represents the "big buckets" of ECS categories. For example, filtering on `event.category:process` yields all events relating to process activity. This field is closely related to `event.type`, which is used as a subcategory. This field is an array. This will allow proper categorization of some events that fall in multiple categories.', + example: 'authentication', + name: 'event.category', + type: 'keyword', + }, + 'event.code': { + category: 'event', + description: + 'Identification code for this event, if one exists. Some event sources use event codes to identify messages unambiguously, regardless of message language or wording adjustments over time. An example of this is the Windows Event ID.', + example: 4648, + name: 'event.code', + type: 'keyword', + }, + 'event.created': { + category: 'event', + description: + "event.created contains the date/time when the event was first read by an agent, or by your pipeline. This field is distinct from @timestamp in that @timestamp typically contain the time extracted from the original event. In most situations, these two timestamps will be slightly different. The difference can be used to calculate the delay between your source generating an event, and the time when your agent first processed it. This can be used to monitor your agent's or pipeline's ability to keep up with your event source. In case the two timestamps are identical, @timestamp should be used.", + example: '2016-05-23T08:05:34.857Z', + name: 'event.created', + type: 'date', + }, + 'event.dataset': { + category: 'event', + description: + "Name of the dataset. If an event source publishes more than one type of log or events (e.g. access log, error log), the dataset is used to specify which one the event comes from. It's recommended but not required to start the dataset name with the module name, followed by a dot, then the dataset name.", + example: 'apache.access', + name: 'event.dataset', + type: 'keyword', + }, + 'event.duration': { + category: 'event', + description: + 'Duration of the event in nanoseconds. If event.start and event.end are known this value should be the difference between the end and start time.', + name: 'event.duration', + type: 'long', + format: 'duration', + }, + 'event.end': { + category: 'event', + description: + 'event.end contains the date when the event ended or when the activity was last observed.', + name: 'event.end', + type: 'date', + }, + 'event.hash': { + category: 'event', + description: + 'Hash (perhaps logstash fingerprint) of raw field to be able to demonstrate log integrity.', + example: '123456789012345678901234567890ABCD', + name: 'event.hash', + type: 'keyword', + }, + 'event.id': { + category: 'event', + description: 'Unique ID to describe the event.', + example: '8a4f500d', + name: 'event.id', + type: 'keyword', + }, + 'event.ingested': { + category: 'event', + description: + "Timestamp when an event arrived in the central data store. This is different from `@timestamp`, which is when the event originally occurred. It's also different from `event.created`, which is meant to capture the first time an agent saw the event. In normal conditions, assuming no tampering, the timestamps should chronologically look like this: `@timestamp` < `event.created` < `event.ingested`.", + example: '2016-05-23T08:05:35.101Z', + name: 'event.ingested', + type: 'date', + }, + 'event.kind': { + category: 'event', + description: + 'This is one of four ECS Categorization Fields, and indicates the highest level in the ECS category hierarchy. `event.kind` gives high-level information about what type of information the event contains, without being specific to the contents of the event. For example, values of this field distinguish alert events from metric events. The value of this field can be used to inform how these kinds of events should be handled. They may warrant different retention, different access control, it may also help understand whether the data coming in at a regular interval or not.', + example: 'alert', + name: 'event.kind', + type: 'keyword', + }, + 'event.module': { + category: 'event', + description: + 'Name of the module this data is coming from. If your monitoring agent supports the concept of modules or plugins to process events of a given source (e.g. Apache logs), `event.module` should contain the name of this module.', + example: 'apache', + name: 'event.module', + type: 'keyword', + }, + 'event.original': { + category: 'event', + description: + 'Raw text message of entire event. Used to demonstrate log integrity. This field is not indexed and doc_values are disabled. It cannot be searched, but it can be retrieved from `_source`.', + example: + 'Sep 19 08:26:10 host CEF:0|Security| threatmanager|1.0|100| worm successfully stopped|10|src=10.0.0.1 dst=2.1.2.2spt=1232', + name: 'event.original', + type: 'keyword', + }, + 'event.outcome': { + category: 'event', + description: + 'This is one of four ECS Categorization Fields, and indicates the lowest level in the ECS category hierarchy. `event.outcome` simply denotes whether the event represents a success or a failure from the perspective of the entity that produced the event. Note that when a single transaction is described in multiple events, each event may populate different values of `event.outcome`, according to their perspective. Also note that in the case of a compound event (a single event that contains multiple logical events), this field should be populated with the value that best captures the overall success or failure from the perspective of the event producer. Further note that not all events will have an associated outcome. For example, this field is generally not populated for metric events, events with `event.type:info`, or any events for which an outcome does not make logical sense.', + example: 'success', + name: 'event.outcome', + type: 'keyword', + }, + 'event.provider': { + category: 'event', + description: + 'Source of the event. Event transports such as Syslog or the Windows Event Log typically mention the source of an event. It can be the name of the software that generated the event (e.g. Sysmon, httpd), or of a subsystem of the operating system (kernel, Microsoft-Windows-Security-Auditing).', + example: 'kernel', + name: 'event.provider', + type: 'keyword', + }, + 'event.reference': { + category: 'event', + description: + 'Reference URL linking to additional information about this event. This URL links to a static definition of the this event. Alert events, indicated by `event.kind:alert`, are a common use case for this field.', + example: 'https://system.vendor.com/event/#0001234', + name: 'event.reference', + type: 'keyword', + }, + 'event.risk_score': { + category: 'event', + description: + "Risk score or priority of the event (e.g. security solutions). Use your system's original value here.", + name: 'event.risk_score', + type: 'float', + }, + 'event.risk_score_norm': { + category: 'event', + description: + 'Normalized risk score or priority of the event, on a scale of 0 to 100. This is mainly useful if you use more than one system that assigns risk scores, and you want to see a normalized value across all systems.', + name: 'event.risk_score_norm', + type: 'float', + }, + 'event.sequence': { + category: 'event', + description: + 'Sequence number of the event. The sequence number is a value published by some event sources, to make the exact ordering of events unambiguous, regardless of the timestamp precision.', + name: 'event.sequence', + type: 'long', + format: 'string', + }, + 'event.severity': { + category: 'event', + description: + "The numeric severity of the event according to your event source. What the different severity values mean can be different between sources and use cases. It's up to the implementer to make sure severities are consistent across events from the same source. The Syslog severity belongs in `log.syslog.severity.code`. `event.severity` is meant to represent the severity according to the event source (e.g. firewall, IDS). If the event source does not publish its own severity, you may optionally copy the `log.syslog.severity.code` to `event.severity`.", + example: 7, + name: 'event.severity', + type: 'long', + format: 'string', + }, + 'event.start': { + category: 'event', + description: + 'event.start contains the date when the event started or when the activity was first observed.', + name: 'event.start', + type: 'date', + }, + 'event.timezone': { + category: 'event', + description: + 'This field should be populated when the event\'s timestamp does not include timezone information already (e.g. default Syslog timestamps). It\'s optional otherwise. Acceptable timezone formats are: a canonical ID (e.g. "Europe/Amsterdam"), abbreviated (e.g. "EST") or an HH:mm differential (e.g. "-05:00").', + name: 'event.timezone', + type: 'keyword', + }, + 'event.type': { + category: 'event', + description: + 'This is one of four ECS Categorization Fields, and indicates the third level in the ECS category hierarchy. `event.type` represents a categorization "sub-bucket" that, when used along with the `event.category` field values, enables filtering events down to a level appropriate for single visualization. This field is an array. This will allow proper categorization of some events that fall in multiple event types.', + name: 'event.type', + type: 'keyword', + }, + 'event.url': { + category: 'event', + description: + 'URL linking to an external system to continue investigation of this event. This URL links to another system where in-depth investigation of the specific occurence of this event can take place. Alert events, indicated by `event.kind:alert`, are a common use case for this field.', + example: 'https://mysystem.mydomain.com/alert/5271dedb-f5b0-4218-87f0-4ac4870a38fe', + name: 'event.url', + type: 'keyword', + }, + 'file.accessed': { + category: 'file', + description: + 'Last time the file was accessed. Note that not all filesystems keep track of access time.', + name: 'file.accessed', + type: 'date', + }, + 'file.attributes': { + category: 'file', + description: + "Array of file attributes. Attributes names will vary by platform. Here's a non-exhaustive list of values that are expected in this field: archive, compressed, directory, encrypted, execute, hidden, read, readonly, system, write.", + example: '["readonly", "system"]', + name: 'file.attributes', + type: 'keyword', + }, + 'file.code_signature.exists': { + category: 'file', + description: 'Boolean to capture if a signature is present.', + example: 'true', + name: 'file.code_signature.exists', + type: 'boolean', + }, + 'file.code_signature.status': { + category: 'file', + description: + 'Additional information about the certificate status. This is useful for logging cryptographic errors with the certificate validity or trust status. Leave unpopulated if the validity or trust of the certificate was unchecked.', + example: 'ERROR_UNTRUSTED_ROOT', + name: 'file.code_signature.status', + type: 'keyword', + }, + 'file.code_signature.subject_name': { + category: 'file', + description: 'Subject name of the code signer', + example: 'Microsoft Corporation', + name: 'file.code_signature.subject_name', + type: 'keyword', + }, + 'file.code_signature.trusted': { + category: 'file', + description: + 'Stores the trust status of the certificate chain. Validating the trust of the certificate chain may be complicated, and this field should only be populated by tools that actively check the status.', + example: 'true', + name: 'file.code_signature.trusted', + type: 'boolean', + }, + 'file.code_signature.valid': { + category: 'file', + description: + 'Boolean to capture if the digital signature is verified against the binary content. Leave unpopulated if a certificate was unchecked.', + example: 'true', + name: 'file.code_signature.valid', + type: 'boolean', + }, + 'file.created': { + category: 'file', + description: 'File creation time. Note that not all filesystems store the creation time.', + name: 'file.created', + type: 'date', + }, + 'file.ctime': { + category: 'file', + description: + 'Last time the file attributes or metadata changed. Note that changes to the file content will update `mtime`. This implies `ctime` will be adjusted at the same time, since `mtime` is an attribute of the file.', + name: 'file.ctime', + type: 'date', + }, + 'file.device': { + category: 'file', + description: 'Device that is the source of the file.', + example: 'sda', + name: 'file.device', + type: 'keyword', + }, + 'file.directory': { + category: 'file', + description: + 'Directory where the file is located. It should include the drive letter, when appropriate.', + example: '/home/alice', + name: 'file.directory', + type: 'keyword', + }, + 'file.drive_letter': { + category: 'file', + description: + 'Drive letter where the file is located. This field is only relevant on Windows. The value should be uppercase, and not include the colon.', + example: 'C', + name: 'file.drive_letter', + type: 'keyword', + }, + 'file.extension': { + category: 'file', + description: 'File extension.', + example: 'png', + name: 'file.extension', + type: 'keyword', + }, + 'file.gid': { + category: 'file', + description: 'Primary group ID (GID) of the file.', + example: '1001', + name: 'file.gid', + type: 'keyword', + }, + 'file.group': { + category: 'file', + description: 'Primary group name of the file.', + example: 'alice', + name: 'file.group', + type: 'keyword', + }, + 'file.hash.md5': { + category: 'file', + description: 'MD5 hash.', + name: 'file.hash.md5', + type: 'keyword', + }, + 'file.hash.sha1': { + category: 'file', + description: 'SHA1 hash.', + name: 'file.hash.sha1', + type: 'keyword', + }, + 'file.hash.sha256': { + category: 'file', + description: 'SHA256 hash.', + name: 'file.hash.sha256', + type: 'keyword', + }, + 'file.hash.sha512': { + category: 'file', + description: 'SHA512 hash.', + name: 'file.hash.sha512', + type: 'keyword', + }, + 'file.inode': { + category: 'file', + description: 'Inode representing the file in the filesystem.', + example: '256383', + name: 'file.inode', + type: 'keyword', + }, + 'file.mime_type': { + category: 'file', + description: + 'MIME type should identify the format of the file or stream of bytes using https://www.iana.org/assignments/media-types/media-types.xhtml[IANA official types], where possible. When more than one type is applicable, the most specific type should be used.', + name: 'file.mime_type', + type: 'keyword', + }, + 'file.mode': { + category: 'file', + description: 'Mode of the file in octal representation.', + example: '0640', + name: 'file.mode', + type: 'keyword', + }, + 'file.mtime': { + category: 'file', + description: 'Last time the file content was modified.', + name: 'file.mtime', + type: 'date', + }, + 'file.name': { + category: 'file', + description: 'Name of the file including the extension, without the directory.', + example: 'example.png', + name: 'file.name', + type: 'keyword', + }, + 'file.owner': { + category: 'file', + description: "File owner's username.", + example: 'alice', + name: 'file.owner', + type: 'keyword', + }, + 'file.path': { + category: 'file', + description: + 'Full path to the file, including the file name. It should include the drive letter, when appropriate.', + example: '/home/alice/example.png', + name: 'file.path', + type: 'keyword', + }, + 'file.pe.company': { + category: 'file', + description: 'Internal company name of the file, provided at compile-time.', + example: 'Microsoft Corporation', + name: 'file.pe.company', + type: 'keyword', + }, + 'file.pe.description': { + category: 'file', + description: 'Internal description of the file, provided at compile-time.', + example: 'Paint', + name: 'file.pe.description', + type: 'keyword', + }, + 'file.pe.file_version': { + category: 'file', + description: 'Internal version of the file, provided at compile-time.', + example: '6.3.9600.17415', + name: 'file.pe.file_version', + type: 'keyword', + }, + 'file.pe.original_file_name': { + category: 'file', + description: 'Internal name of the file, provided at compile-time.', + example: 'MSPAINT.EXE', + name: 'file.pe.original_file_name', + type: 'keyword', + }, + 'file.pe.product': { + category: 'file', + description: 'Internal product name of the file, provided at compile-time.', + example: 'Microsoft® Windows® Operating System', + name: 'file.pe.product', + type: 'keyword', + }, + 'file.size': { + category: 'file', + description: 'File size in bytes. Only relevant when `file.type` is "file".', + example: 16384, + name: 'file.size', + type: 'long', + }, + 'file.target_path': { + category: 'file', + description: 'Target path for symlinks.', + name: 'file.target_path', + type: 'keyword', + }, + 'file.type': { + category: 'file', + description: 'File type (file, dir, or symlink).', + example: 'file', + name: 'file.type', + type: 'keyword', + }, + 'file.uid': { + category: 'file', + description: 'The user ID (UID) or security identifier (SID) of the file owner.', + example: '1001', + name: 'file.uid', + type: 'keyword', + }, + 'geo.city_name': { + category: 'geo', + description: 'City name.', + example: 'Montreal', + name: 'geo.city_name', + type: 'keyword', + }, + 'geo.continent_name': { + category: 'geo', + description: 'Name of the continent.', + example: 'North America', + name: 'geo.continent_name', + type: 'keyword', + }, + 'geo.country_iso_code': { + category: 'geo', + description: 'Country ISO code.', + example: 'CA', + name: 'geo.country_iso_code', + type: 'keyword', + }, + 'geo.country_name': { + category: 'geo', + description: 'Country name.', + example: 'Canada', + name: 'geo.country_name', + type: 'keyword', + }, + 'geo.location': { + category: 'geo', + description: 'Longitude and latitude.', + example: '{ "lon": -73.614830, "lat": 45.505918 }', + name: 'geo.location', + type: 'geo_point', + }, + 'geo.name': { + category: 'geo', + description: + 'User-defined description of a location, at the level of granularity they care about. Could be the name of their data centers, the floor number, if this describes a local physical entity, city names. Not typically used in automated geolocation.', + example: 'boston-dc', + name: 'geo.name', + type: 'keyword', + }, + 'geo.region_iso_code': { + category: 'geo', + description: 'Region ISO code.', + example: 'CA-QC', + name: 'geo.region_iso_code', + type: 'keyword', + }, + 'geo.region_name': { + category: 'geo', + description: 'Region name.', + example: 'Quebec', + name: 'geo.region_name', + type: 'keyword', + }, + 'group.domain': { + category: 'group', + description: + 'Name of the directory the group is a member of. For example, an LDAP or Active Directory domain name.', + name: 'group.domain', + type: 'keyword', + }, + 'group.id': { + category: 'group', + description: 'Unique identifier for the group on the system/platform.', + name: 'group.id', + type: 'keyword', + }, + 'group.name': { + category: 'group', + description: 'Name of the group.', + name: 'group.name', + type: 'keyword', + }, + 'hash.md5': { + category: 'hash', + description: 'MD5 hash.', + name: 'hash.md5', + type: 'keyword', + }, + 'hash.sha1': { + category: 'hash', + description: 'SHA1 hash.', + name: 'hash.sha1', + type: 'keyword', + }, + 'hash.sha256': { + category: 'hash', + description: 'SHA256 hash.', + name: 'hash.sha256', + type: 'keyword', + }, + 'hash.sha512': { + category: 'hash', + description: 'SHA512 hash.', + name: 'hash.sha512', + type: 'keyword', + }, + 'host.architecture': { + category: 'host', + description: 'Operating system architecture.', + example: 'x86_64', + name: 'host.architecture', + type: 'keyword', + }, + 'host.domain': { + category: 'host', + description: + "Name of the domain of which the host is a member. For example, on Windows this could be the host's Active Directory domain or NetBIOS domain name. For Linux this could be the domain of the host's LDAP provider.", + example: 'CONTOSO', + name: 'host.domain', + type: 'keyword', + }, + 'host.geo.city_name': { + category: 'host', + description: 'City name.', + example: 'Montreal', + name: 'host.geo.city_name', + type: 'keyword', + }, + 'host.geo.continent_name': { + category: 'host', + description: 'Name of the continent.', + example: 'North America', + name: 'host.geo.continent_name', + type: 'keyword', + }, + 'host.geo.country_iso_code': { + category: 'host', + description: 'Country ISO code.', + example: 'CA', + name: 'host.geo.country_iso_code', + type: 'keyword', + }, + 'host.geo.country_name': { + category: 'host', + description: 'Country name.', + example: 'Canada', + name: 'host.geo.country_name', + type: 'keyword', + }, + 'host.geo.location': { + category: 'host', + description: 'Longitude and latitude.', + example: '{ "lon": -73.614830, "lat": 45.505918 }', + name: 'host.geo.location', + type: 'geo_point', + }, + 'host.geo.name': { + category: 'host', + description: + 'User-defined description of a location, at the level of granularity they care about. Could be the name of their data centers, the floor number, if this describes a local physical entity, city names. Not typically used in automated geolocation.', + example: 'boston-dc', + name: 'host.geo.name', + type: 'keyword', + }, + 'host.geo.region_iso_code': { + category: 'host', + description: 'Region ISO code.', + example: 'CA-QC', + name: 'host.geo.region_iso_code', + type: 'keyword', + }, + 'host.geo.region_name': { + category: 'host', + description: 'Region name.', + example: 'Quebec', + name: 'host.geo.region_name', + type: 'keyword', + }, + 'host.hostname': { + category: 'host', + description: + 'Hostname of the host. It normally contains what the `hostname` command returns on the host machine.', + name: 'host.hostname', + type: 'keyword', + }, + 'host.id': { + category: 'host', + description: + 'Unique host id. As hostname is not always unique, use values that are meaningful in your environment. Example: The current usage of `beat.name`.', + name: 'host.id', + type: 'keyword', + }, + 'host.ip': { + category: 'host', + description: 'Host ip addresses.', + name: 'host.ip', + type: 'ip', + }, + 'host.mac': { + category: 'host', + description: 'Host mac addresses.', + name: 'host.mac', + type: 'keyword', + }, + 'host.name': { + category: 'host', + description: + 'Name of the host. It can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.', + name: 'host.name', + type: 'keyword', + }, + 'host.os.family': { + category: 'host', + description: 'OS family (such as redhat, debian, freebsd, windows).', + example: 'debian', + name: 'host.os.family', + type: 'keyword', + }, + 'host.os.full': { + category: 'host', + description: 'Operating system name, including the version or code name.', + example: 'Mac OS Mojave', + name: 'host.os.full', + type: 'keyword', + }, + 'host.os.kernel': { + category: 'host', + description: 'Operating system kernel version as a raw string.', + example: '4.4.0-112-generic', + name: 'host.os.kernel', + type: 'keyword', + }, + 'host.os.name': { + category: 'host', + description: 'Operating system name, without the version.', + example: 'Mac OS X', + name: 'host.os.name', + type: 'keyword', + }, + 'host.os.platform': { + category: 'host', + description: 'Operating system platform (such centos, ubuntu, windows).', + example: 'darwin', + name: 'host.os.platform', + type: 'keyword', + }, + 'host.os.version': { + category: 'host', + description: 'Operating system version as a raw string.', + example: '10.14.1', + name: 'host.os.version', + type: 'keyword', + }, + 'host.type': { + category: 'host', + description: + 'Type of host. For Cloud providers this can be the machine type like `t2.medium`. If vm, this could be the container, for example, or other information meaningful in your environment.', + name: 'host.type', + type: 'keyword', + }, + 'host.uptime': { + category: 'host', + description: 'Seconds the host has been up.', + example: 1325, + name: 'host.uptime', + type: 'long', + }, + 'host.user.domain': { + category: 'host', + description: + 'Name of the directory the user is a member of. For example, an LDAP or Active Directory domain name.', + name: 'host.user.domain', + type: 'keyword', + }, + 'host.user.email': { + category: 'host', + description: 'User email address.', + name: 'host.user.email', + type: 'keyword', + }, + 'host.user.full_name': { + category: 'host', + description: "User's full name, if available.", + example: 'Albert Einstein', + name: 'host.user.full_name', + type: 'keyword', + }, + 'host.user.group.domain': { + category: 'host', + description: + 'Name of the directory the group is a member of. For example, an LDAP or Active Directory domain name.', + name: 'host.user.group.domain', + type: 'keyword', + }, + 'host.user.group.id': { + category: 'host', + description: 'Unique identifier for the group on the system/platform.', + name: 'host.user.group.id', + type: 'keyword', + }, + 'host.user.group.name': { + category: 'host', + description: 'Name of the group.', + name: 'host.user.group.name', + type: 'keyword', + }, + 'host.user.hash': { + category: 'host', + description: + 'Unique user hash to correlate information for a user in anonymized form. Useful if `user.id` or `user.name` contain confidential information and cannot be used.', + name: 'host.user.hash', + type: 'keyword', + }, + 'host.user.id': { + category: 'host', + description: 'Unique identifiers of the user.', + name: 'host.user.id', + type: 'keyword', + }, + 'host.user.name': { + category: 'host', + description: 'Short name or login of the user.', + example: 'albert', + name: 'host.user.name', + type: 'keyword', + }, + 'http.request.body.bytes': { + category: 'http', + description: 'Size in bytes of the request body.', + example: 887, + name: 'http.request.body.bytes', + type: 'long', + format: 'bytes', + }, + 'http.request.body.content': { + category: 'http', + description: 'The full HTTP request body.', + example: 'Hello world', + name: 'http.request.body.content', + type: 'keyword', + }, + 'http.request.bytes': { + category: 'http', + description: 'Total size in bytes of the request (body and headers).', + example: 1437, + name: 'http.request.bytes', + type: 'long', + format: 'bytes', + }, + 'http.request.method': { + category: 'http', + description: + 'HTTP request method. The field value must be normalized to lowercase for querying. See the documentation section "Implementing ECS".', + example: 'get, post, put', + name: 'http.request.method', + type: 'keyword', + }, + 'http.request.referrer': { + category: 'http', + description: 'Referrer for this HTTP request.', + example: 'https://blog.example.com/', + name: 'http.request.referrer', + type: 'keyword', + }, + 'http.response.body.bytes': { + category: 'http', + description: 'Size in bytes of the response body.', + example: 887, + name: 'http.response.body.bytes', + type: 'long', + format: 'bytes', + }, + 'http.response.body.content': { + category: 'http', + description: 'The full HTTP response body.', + example: 'Hello world', + name: 'http.response.body.content', + type: 'keyword', + }, + 'http.response.bytes': { + category: 'http', + description: 'Total size in bytes of the response (body and headers).', + example: 1437, + name: 'http.response.bytes', + type: 'long', + format: 'bytes', + }, + 'http.response.status_code': { + category: 'http', + description: 'HTTP response status code.', + example: 404, + name: 'http.response.status_code', + type: 'long', + format: 'string', + }, + 'http.version': { + category: 'http', + description: 'HTTP version.', + example: 1.1, + name: 'http.version', + type: 'keyword', + }, + 'interface.alias': { + category: 'interface', + description: + 'Interface alias as reported by the system, typically used in firewall implementations for e.g. inside, outside, or dmz logical interface naming.', + example: 'outside', + name: 'interface.alias', + type: 'keyword', + }, + 'interface.id': { + category: 'interface', + description: 'Interface ID as reported by an observer (typically SNMP interface ID).', + example: 10, + name: 'interface.id', + type: 'keyword', + }, + 'interface.name': { + category: 'interface', + description: 'Interface name as reported by the system.', + example: 'eth0', + name: 'interface.name', + type: 'keyword', + }, + 'log.level': { + category: 'log', + description: + "Original log level of the log event. If the source of the event provides a log level or textual severity, this is the one that goes in `log.level`. If your source doesn't specify one, you may put your event transport's severity here (e.g. Syslog severity). Some examples are `warn`, `err`, `i`, `informational`.", + example: 'error', + name: 'log.level', + type: 'keyword', + }, + 'log.logger': { + category: 'log', + description: + 'The name of the logger inside an application. This is usually the name of the class which initialized the logger, or can be a custom name.', + example: 'org.elasticsearch.bootstrap.Bootstrap', + name: 'log.logger', + type: 'keyword', + }, + 'log.origin.file.line': { + category: 'log', + description: + 'The line number of the file containing the source code which originated the log event.', + example: 42, + name: 'log.origin.file.line', + type: 'integer', + }, + 'log.origin.file.name': { + category: 'log', + description: + 'The name of the file containing the source code which originated the log event. Note that this is not the name of the log file.', + example: 'Bootstrap.java', + name: 'log.origin.file.name', + type: 'keyword', + }, + 'log.origin.function': { + category: 'log', + description: 'The name of the function or method which originated the log event.', + example: 'init', + name: 'log.origin.function', + type: 'keyword', + }, + 'log.original': { + category: 'log', + description: + "This is the original log message and contains the full log message before splitting it up in multiple parts. In contrast to the `message` field which can contain an extracted part of the log message, this field contains the original, full log message. It can have already some modifications applied like encoding or new lines removed to clean up the log message. This field is not indexed and doc_values are disabled so it can't be queried but the value can be retrieved from `_source`.", + example: 'Sep 19 08:26:10 localhost My log', + name: 'log.original', + type: 'keyword', + }, + 'log.syslog': { + category: 'log', + description: + 'The Syslog metadata of the event, if the event was transmitted via Syslog. Please see RFCs 5424 or 3164.', + name: 'log.syslog', + type: 'object', + }, + 'log.syslog.facility.code': { + category: 'log', + description: + 'The Syslog numeric facility of the log event, if available. According to RFCs 5424 and 3164, this value should be an integer between 0 and 23.', + example: 23, + name: 'log.syslog.facility.code', + type: 'long', + format: 'string', + }, + 'log.syslog.facility.name': { + category: 'log', + description: 'The Syslog text-based facility of the log event, if available.', + example: 'local7', + name: 'log.syslog.facility.name', + type: 'keyword', + }, + 'log.syslog.priority': { + category: 'log', + description: + 'Syslog numeric priority of the event, if available. According to RFCs 5424 and 3164, the priority is 8 * facility + severity. This number is therefore expected to contain a value between 0 and 191.', + example: 135, + name: 'log.syslog.priority', + type: 'long', + format: 'string', + }, + 'log.syslog.severity.code': { + category: 'log', + description: + "The Syslog numeric severity of the log event, if available. If the event source publishing via Syslog provides a different numeric severity value (e.g. firewall, IDS), your source's numeric severity should go to `event.severity`. If the event source does not specify a distinct severity, you can optionally copy the Syslog severity to `event.severity`.", + example: 3, + name: 'log.syslog.severity.code', + type: 'long', + }, + 'log.syslog.severity.name': { + category: 'log', + description: + "The Syslog numeric severity of the log event, if available. If the event source publishing via Syslog provides a different severity value (e.g. firewall, IDS), your source's text severity should go to `log.level`. If the event source does not specify a distinct severity, you can optionally copy the Syslog severity to `log.level`.", + example: 'Error', + name: 'log.syslog.severity.name', + type: 'keyword', + }, + 'network.application': { + category: 'network', + description: + 'A name given to an application level protocol. This can be arbitrarily assigned for things like microservices, but also apply to things like skype, icq, facebook, twitter. This would be used in situations where the vendor or service can be decoded such as from the source/dest IP owners, ports, or wire format. The field value must be normalized to lowercase for querying. See the documentation section "Implementing ECS".', + example: 'aim', + name: 'network.application', + type: 'keyword', + }, + 'network.bytes': { + category: 'network', + description: + 'Total bytes transferred in both directions. If `source.bytes` and `destination.bytes` are known, `network.bytes` is their sum.', + example: 368, + name: 'network.bytes', + type: 'long', + format: 'bytes', + }, + 'network.community_id': { + category: 'network', + description: + 'A hash of source and destination IPs and ports, as well as the protocol used in a communication. This is a tool-agnostic standard to identify flows. Learn more at https://github.com/corelight/community-id-spec.', + example: '1:hO+sN4H+MG5MY/8hIrXPqc4ZQz0=', + name: 'network.community_id', + type: 'keyword', + }, + 'network.direction': { + category: 'network', + description: + "Direction of the network traffic. Recommended values are: * inbound * outbound * internal * external * unknown When mapping events from a host-based monitoring context, populate this field from the host's point of view. When mapping events from a network or perimeter-based monitoring context, populate this field from the point of view of your network perimeter.", + example: 'inbound', + name: 'network.direction', + type: 'keyword', + }, + 'network.forwarded_ip': { + category: 'network', + description: 'Host IP address when the source IP address is the proxy.', + example: '192.1.1.2', + name: 'network.forwarded_ip', + type: 'ip', + }, + 'network.iana_number': { + category: 'network', + description: + 'IANA Protocol Number (https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml). Standardized list of protocols. This aligns well with NetFlow and sFlow related logs which use the IANA Protocol Number.', + example: 6, + name: 'network.iana_number', + type: 'keyword', + }, + 'network.inner': { + category: 'network', + description: + 'Network.inner fields are added in addition to network.vlan fields to describe the innermost VLAN when q-in-q VLAN tagging is present. Allowed fields include vlan.id and vlan.name. Inner vlan fields are typically used when sending traffic with multiple 802.1q encapsulations to a network sensor (e.g. Zeek, Wireshark.)', + name: 'network.inner', + type: 'object', + }, + 'network.inner.vlan.id': { + category: 'network', + description: 'VLAN ID as reported by the observer.', + example: 10, + name: 'network.inner.vlan.id', + type: 'keyword', + }, + 'network.inner.vlan.name': { + category: 'network', + description: 'Optional VLAN name as reported by the observer.', + example: 'outside', + name: 'network.inner.vlan.name', + type: 'keyword', + }, + 'network.name': { + category: 'network', + description: 'Name given by operators to sections of their network.', + example: 'Guest Wifi', + name: 'network.name', + type: 'keyword', + }, + 'network.packets': { + category: 'network', + description: + 'Total packets transferred in both directions. If `source.packets` and `destination.packets` are known, `network.packets` is their sum.', + example: 24, + name: 'network.packets', + type: 'long', + }, + 'network.protocol': { + category: 'network', + description: + 'L7 Network protocol name. ex. http, lumberjack, transport protocol. The field value must be normalized to lowercase for querying. See the documentation section "Implementing ECS".', + example: 'http', + name: 'network.protocol', + type: 'keyword', + }, + 'network.transport': { + category: 'network', + description: + 'Same as network.iana_number, but instead using the Keyword name of the transport layer (udp, tcp, ipv6-icmp, etc.) The field value must be normalized to lowercase for querying. See the documentation section "Implementing ECS".', + example: 'tcp', + name: 'network.transport', + type: 'keyword', + }, + 'network.type': { + category: 'network', + description: + 'In the OSI Model this would be the Network Layer. ipv4, ipv6, ipsec, pim, etc The field value must be normalized to lowercase for querying. See the documentation section "Implementing ECS".', + example: 'ipv4', + name: 'network.type', + type: 'keyword', + }, + 'network.vlan.id': { + category: 'network', + description: 'VLAN ID as reported by the observer.', + example: 10, + name: 'network.vlan.id', + type: 'keyword', + }, + 'network.vlan.name': { + category: 'network', + description: 'Optional VLAN name as reported by the observer.', + example: 'outside', + name: 'network.vlan.name', + type: 'keyword', + }, + 'observer.egress': { + category: 'observer', + description: + 'Observer.egress holds information like interface number and name, vlan, and zone information to classify egress traffic. Single armed monitoring such as a network sensor on a span port should only use observer.ingress to categorize traffic.', + name: 'observer.egress', + type: 'object', + }, + 'observer.egress.interface.alias': { + category: 'observer', + description: + 'Interface alias as reported by the system, typically used in firewall implementations for e.g. inside, outside, or dmz logical interface naming.', + example: 'outside', + name: 'observer.egress.interface.alias', + type: 'keyword', + }, + 'observer.egress.interface.id': { + category: 'observer', + description: 'Interface ID as reported by an observer (typically SNMP interface ID).', + example: 10, + name: 'observer.egress.interface.id', + type: 'keyword', + }, + 'observer.egress.interface.name': { + category: 'observer', + description: 'Interface name as reported by the system.', + example: 'eth0', + name: 'observer.egress.interface.name', + type: 'keyword', + }, + 'observer.egress.vlan.id': { + category: 'observer', + description: 'VLAN ID as reported by the observer.', + example: 10, + name: 'observer.egress.vlan.id', + type: 'keyword', + }, + 'observer.egress.vlan.name': { + category: 'observer', + description: 'Optional VLAN name as reported by the observer.', + example: 'outside', + name: 'observer.egress.vlan.name', + type: 'keyword', + }, + 'observer.egress.zone': { + category: 'observer', + description: + 'Network zone of outbound traffic as reported by the observer to categorize the destination area of egress traffic, e.g. Internal, External, DMZ, HR, Legal, etc.', + example: 'Public_Internet', + name: 'observer.egress.zone', + type: 'keyword', + }, + 'observer.geo.city_name': { + category: 'observer', + description: 'City name.', + example: 'Montreal', + name: 'observer.geo.city_name', + type: 'keyword', + }, + 'observer.geo.continent_name': { + category: 'observer', + description: 'Name of the continent.', + example: 'North America', + name: 'observer.geo.continent_name', + type: 'keyword', + }, + 'observer.geo.country_iso_code': { + category: 'observer', + description: 'Country ISO code.', + example: 'CA', + name: 'observer.geo.country_iso_code', + type: 'keyword', + }, + 'observer.geo.country_name': { + category: 'observer', + description: 'Country name.', + example: 'Canada', + name: 'observer.geo.country_name', + type: 'keyword', + }, + 'observer.geo.location': { + category: 'observer', + description: 'Longitude and latitude.', + example: '{ "lon": -73.614830, "lat": 45.505918 }', + name: 'observer.geo.location', + type: 'geo_point', + }, + 'observer.geo.name': { + category: 'observer', + description: + 'User-defined description of a location, at the level of granularity they care about. Could be the name of their data centers, the floor number, if this describes a local physical entity, city names. Not typically used in automated geolocation.', + example: 'boston-dc', + name: 'observer.geo.name', + type: 'keyword', + }, + 'observer.geo.region_iso_code': { + category: 'observer', + description: 'Region ISO code.', + example: 'CA-QC', + name: 'observer.geo.region_iso_code', + type: 'keyword', + }, + 'observer.geo.region_name': { + category: 'observer', + description: 'Region name.', + example: 'Quebec', + name: 'observer.geo.region_name', + type: 'keyword', + }, + 'observer.hostname': { + category: 'observer', + description: 'Hostname of the observer.', + name: 'observer.hostname', + type: 'keyword', + }, + 'observer.ingress': { + category: 'observer', + description: + 'Observer.ingress holds information like interface number and name, vlan, and zone information to classify ingress traffic. Single armed monitoring such as a network sensor on a span port should only use observer.ingress to categorize traffic.', + name: 'observer.ingress', + type: 'object', + }, + 'observer.ingress.interface.alias': { + category: 'observer', + description: + 'Interface alias as reported by the system, typically used in firewall implementations for e.g. inside, outside, or dmz logical interface naming.', + example: 'outside', + name: 'observer.ingress.interface.alias', + type: 'keyword', + }, + 'observer.ingress.interface.id': { + category: 'observer', + description: 'Interface ID as reported by an observer (typically SNMP interface ID).', + example: 10, + name: 'observer.ingress.interface.id', + type: 'keyword', + }, + 'observer.ingress.interface.name': { + category: 'observer', + description: 'Interface name as reported by the system.', + example: 'eth0', + name: 'observer.ingress.interface.name', + type: 'keyword', + }, + 'observer.ingress.vlan.id': { + category: 'observer', + description: 'VLAN ID as reported by the observer.', + example: 10, + name: 'observer.ingress.vlan.id', + type: 'keyword', + }, + 'observer.ingress.vlan.name': { + category: 'observer', + description: 'Optional VLAN name as reported by the observer.', + example: 'outside', + name: 'observer.ingress.vlan.name', + type: 'keyword', + }, + 'observer.ingress.zone': { + category: 'observer', + description: + 'Network zone of incoming traffic as reported by the observer to categorize the source area of ingress traffic. e.g. internal, External, DMZ, HR, Legal, etc.', + example: 'DMZ', + name: 'observer.ingress.zone', + type: 'keyword', + }, + 'observer.ip': { + category: 'observer', + description: 'IP addresses of the observer.', + name: 'observer.ip', + type: 'ip', + }, + 'observer.mac': { + category: 'observer', + description: 'MAC addresses of the observer', + name: 'observer.mac', + type: 'keyword', + }, + 'observer.name': { + category: 'observer', + description: + 'Custom name of the observer. This is a name that can be given to an observer. This can be helpful for example if multiple firewalls of the same model are used in an organization. If no custom name is needed, the field can be left empty.', + example: '1_proxySG', + name: 'observer.name', + type: 'keyword', + }, + 'observer.os.family': { + category: 'observer', + description: 'OS family (such as redhat, debian, freebsd, windows).', + example: 'debian', + name: 'observer.os.family', + type: 'keyword', + }, + 'observer.os.full': { + category: 'observer', + description: 'Operating system name, including the version or code name.', + example: 'Mac OS Mojave', + name: 'observer.os.full', + type: 'keyword', + }, + 'observer.os.kernel': { + category: 'observer', + description: 'Operating system kernel version as a raw string.', + example: '4.4.0-112-generic', + name: 'observer.os.kernel', + type: 'keyword', + }, + 'observer.os.name': { + category: 'observer', + description: 'Operating system name, without the version.', + example: 'Mac OS X', + name: 'observer.os.name', + type: 'keyword', + }, + 'observer.os.platform': { + category: 'observer', + description: 'Operating system platform (such centos, ubuntu, windows).', + example: 'darwin', + name: 'observer.os.platform', + type: 'keyword', + }, + 'observer.os.version': { + category: 'observer', + description: 'Operating system version as a raw string.', + example: '10.14.1', + name: 'observer.os.version', + type: 'keyword', + }, + 'observer.product': { + category: 'observer', + description: 'The product name of the observer.', + example: 's200', + name: 'observer.product', + type: 'keyword', + }, + 'observer.serial_number': { + category: 'observer', + description: 'Observer serial number.', + name: 'observer.serial_number', + type: 'keyword', + }, + 'observer.type': { + category: 'observer', + description: + 'The type of the observer the data is coming from. There is no predefined list of observer types. Some examples are `forwarder`, `firewall`, `ids`, `ips`, `proxy`, `poller`, `sensor`, `APM server`.', + example: 'firewall', + name: 'observer.type', + type: 'keyword', + }, + 'observer.vendor': { + category: 'observer', + description: 'Vendor name of the observer.', + example: 'Symantec', + name: 'observer.vendor', + type: 'keyword', + }, + 'observer.version': { + category: 'observer', + description: 'Observer version.', + name: 'observer.version', + type: 'keyword', + }, + 'organization.id': { + category: 'organization', + description: 'Unique identifier for the organization.', + name: 'organization.id', + type: 'keyword', + }, + 'organization.name': { + category: 'organization', + description: 'Organization name.', + name: 'organization.name', + type: 'keyword', + }, + 'os.family': { + category: 'os', + description: 'OS family (such as redhat, debian, freebsd, windows).', + example: 'debian', + name: 'os.family', + type: 'keyword', + }, + 'os.full': { + category: 'os', + description: 'Operating system name, including the version or code name.', + example: 'Mac OS Mojave', + name: 'os.full', + type: 'keyword', + }, + 'os.kernel': { + category: 'os', + description: 'Operating system kernel version as a raw string.', + example: '4.4.0-112-generic', + name: 'os.kernel', + type: 'keyword', + }, + 'os.name': { + category: 'os', + description: 'Operating system name, without the version.', + example: 'Mac OS X', + name: 'os.name', + type: 'keyword', + }, + 'os.platform': { + category: 'os', + description: 'Operating system platform (such centos, ubuntu, windows).', + example: 'darwin', + name: 'os.platform', + type: 'keyword', + }, + 'os.version': { + category: 'os', + description: 'Operating system version as a raw string.', + example: '10.14.1', + name: 'os.version', + type: 'keyword', + }, + 'package.architecture': { + category: 'package', + description: 'Package architecture.', + example: 'x86_64', + name: 'package.architecture', + type: 'keyword', + }, + 'package.build_version': { + category: 'package', + description: + 'Additional information about the build version of the installed package. For example use the commit SHA of a non-released package.', + example: '36f4f7e89dd61b0988b12ee000b98966867710cd', + name: 'package.build_version', + type: 'keyword', + }, + 'package.checksum': { + category: 'package', + description: 'Checksum of the installed package for verification.', + example: '68b329da9893e34099c7d8ad5cb9c940', + name: 'package.checksum', + type: 'keyword', + }, + 'package.description': { + category: 'package', + description: 'Description of the package.', + example: 'Open source programming language to build simple/reliable/efficient software.', + name: 'package.description', + type: 'keyword', + }, + 'package.install_scope': { + category: 'package', + description: 'Indicating how the package was installed, e.g. user-local, global.', + example: 'global', + name: 'package.install_scope', + type: 'keyword', + }, + 'package.installed': { + category: 'package', + description: 'Time when package was installed.', + name: 'package.installed', + type: 'date', + }, + 'package.license': { + category: 'package', + description: + 'License under which the package was released. Use a short name, e.g. the license identifier from SPDX License List where possible (https://spdx.org/licenses/).', + example: 'Apache License 2.0', + name: 'package.license', + type: 'keyword', + }, + 'package.name': { + category: 'package', + description: 'Package name', + example: 'go', + name: 'package.name', + type: 'keyword', + }, + 'package.path': { + category: 'package', + description: 'Path where the package is installed.', + example: '/usr/local/Cellar/go/1.12.9/', + name: 'package.path', + type: 'keyword', + }, + 'package.reference': { + category: 'package', + description: 'Home page or reference URL of the software in this package, if available.', + example: 'https://golang.org', + name: 'package.reference', + type: 'keyword', + }, + 'package.size': { + category: 'package', + description: 'Package size in bytes.', + example: 62231, + name: 'package.size', + type: 'long', + format: 'string', + }, + 'package.type': { + category: 'package', + description: + 'Type of package. This should contain the package file type, rather than the package manager name. Examples: rpm, dpkg, brew, npm, gem, nupkg, jar.', + example: 'rpm', + name: 'package.type', + type: 'keyword', + }, + 'package.version': { + category: 'package', + description: 'Package version', + example: '1.12.9', + name: 'package.version', + type: 'keyword', + }, + 'pe.company': { + category: 'pe', + description: 'Internal company name of the file, provided at compile-time.', + example: 'Microsoft Corporation', + name: 'pe.company', + type: 'keyword', + }, + 'pe.description': { + category: 'pe', + description: 'Internal description of the file, provided at compile-time.', + example: 'Paint', + name: 'pe.description', + type: 'keyword', + }, + 'pe.file_version': { + category: 'pe', + description: 'Internal version of the file, provided at compile-time.', + example: '6.3.9600.17415', + name: 'pe.file_version', + type: 'keyword', + }, + 'pe.original_file_name': { + category: 'pe', + description: 'Internal name of the file, provided at compile-time.', + example: 'MSPAINT.EXE', + name: 'pe.original_file_name', + type: 'keyword', + }, + 'pe.product': { + category: 'pe', + description: 'Internal product name of the file, provided at compile-time.', + example: 'Microsoft® Windows® Operating System', + name: 'pe.product', + type: 'keyword', + }, + 'process.args': { + category: 'process', + description: + 'Array of process arguments, starting with the absolute path to the executable. May be filtered to protect sensitive information.', + example: '["/usr/bin/ssh","-l","user","10.0.0.16"]', + name: 'process.args', + type: 'keyword', + }, + 'process.args_count': { + category: 'process', + description: + 'Length of the process.args array. This field can be useful for querying or performing bucket analysis on how many arguments were provided to start a process. More arguments may be an indication of suspicious activity.', + example: 4, + name: 'process.args_count', + type: 'long', + }, + 'process.code_signature.exists': { + category: 'process', + description: 'Boolean to capture if a signature is present.', + example: 'true', + name: 'process.code_signature.exists', + type: 'boolean', + }, + 'process.code_signature.status': { + category: 'process', + description: + 'Additional information about the certificate status. This is useful for logging cryptographic errors with the certificate validity or trust status. Leave unpopulated if the validity or trust of the certificate was unchecked.', + example: 'ERROR_UNTRUSTED_ROOT', + name: 'process.code_signature.status', + type: 'keyword', + }, + 'process.code_signature.subject_name': { + category: 'process', + description: 'Subject name of the code signer', + example: 'Microsoft Corporation', + name: 'process.code_signature.subject_name', + type: 'keyword', + }, + 'process.code_signature.trusted': { + category: 'process', + description: + 'Stores the trust status of the certificate chain. Validating the trust of the certificate chain may be complicated, and this field should only be populated by tools that actively check the status.', + example: 'true', + name: 'process.code_signature.trusted', + type: 'boolean', + }, + 'process.code_signature.valid': { + category: 'process', + description: + 'Boolean to capture if the digital signature is verified against the binary content. Leave unpopulated if a certificate was unchecked.', + example: 'true', + name: 'process.code_signature.valid', + type: 'boolean', + }, + 'process.command_line': { + category: 'process', + description: + 'Full command line that started the process, including the absolute path to the executable, and all arguments. Some arguments may be filtered to protect sensitive information.', + example: '/usr/bin/ssh -l user 10.0.0.16', + name: 'process.command_line', + type: 'keyword', + }, + 'process.entity_id': { + category: 'process', + description: + 'Unique identifier for the process. The implementation of this is specified by the data source, but some examples of what could be used here are a process-generated UUID, Sysmon Process GUIDs, or a hash of some uniquely identifying components of a process. Constructing a globally unique identifier is a common practice to mitigate PID reuse as well as to identify a specific process over time, across multiple monitored hosts.', + example: 'c2c455d9f99375d', + name: 'process.entity_id', + type: 'keyword', + }, + 'process.executable': { + category: 'process', + description: 'Absolute path to the process executable.', + example: '/usr/bin/ssh', + name: 'process.executable', + type: 'keyword', + }, + 'process.exit_code': { + category: 'process', + description: + 'The exit code of the process, if this is a termination event. The field should be absent if there is no exit code for the event (e.g. process start).', + example: 137, + name: 'process.exit_code', + type: 'long', + }, + 'process.hash.md5': { + category: 'process', + description: 'MD5 hash.', + name: 'process.hash.md5', + type: 'keyword', + }, + 'process.hash.sha1': { + category: 'process', + description: 'SHA1 hash.', + name: 'process.hash.sha1', + type: 'keyword', + }, + 'process.hash.sha256': { + category: 'process', + description: 'SHA256 hash.', + name: 'process.hash.sha256', + type: 'keyword', + }, + 'process.hash.sha512': { + category: 'process', + description: 'SHA512 hash.', + name: 'process.hash.sha512', + type: 'keyword', + }, + 'process.name': { + category: 'process', + description: 'Process name. Sometimes called program name or similar.', + example: 'ssh', + name: 'process.name', + type: 'keyword', + }, + 'process.parent.args': { + category: 'process', + description: 'Array of process arguments. May be filtered to protect sensitive information.', + example: '["ssh","-l","user","10.0.0.16"]', + name: 'process.parent.args', + type: 'keyword', + }, + 'process.parent.args_count': { + category: 'process', + description: + 'Length of the process.args array. This field can be useful for querying or performing bucket analysis on how many arguments were provided to start a process. More arguments may be an indication of suspicious activity.', + example: 4, + name: 'process.parent.args_count', + type: 'long', + }, + 'process.parent.code_signature.exists': { + category: 'process', + description: 'Boolean to capture if a signature is present.', + example: 'true', + name: 'process.parent.code_signature.exists', + type: 'boolean', + }, + 'process.parent.code_signature.status': { + category: 'process', + description: + 'Additional information about the certificate status. This is useful for logging cryptographic errors with the certificate validity or trust status. Leave unpopulated if the validity or trust of the certificate was unchecked.', + example: 'ERROR_UNTRUSTED_ROOT', + name: 'process.parent.code_signature.status', + type: 'keyword', + }, + 'process.parent.code_signature.subject_name': { + category: 'process', + description: 'Subject name of the code signer', + example: 'Microsoft Corporation', + name: 'process.parent.code_signature.subject_name', + type: 'keyword', + }, + 'process.parent.code_signature.trusted': { + category: 'process', + description: + 'Stores the trust status of the certificate chain. Validating the trust of the certificate chain may be complicated, and this field should only be populated by tools that actively check the status.', + example: 'true', + name: 'process.parent.code_signature.trusted', + type: 'boolean', + }, + 'process.parent.code_signature.valid': { + category: 'process', + description: + 'Boolean to capture if the digital signature is verified against the binary content. Leave unpopulated if a certificate was unchecked.', + example: 'true', + name: 'process.parent.code_signature.valid', + type: 'boolean', + }, + 'process.parent.command_line': { + category: 'process', + description: + 'Full command line that started the process, including the absolute path to the executable, and all arguments. Some arguments may be filtered to protect sensitive information.', + example: '/usr/bin/ssh -l user 10.0.0.16', + name: 'process.parent.command_line', + type: 'keyword', + }, + 'process.parent.entity_id': { + category: 'process', + description: + 'Unique identifier for the process. The implementation of this is specified by the data source, but some examples of what could be used here are a process-generated UUID, Sysmon Process GUIDs, or a hash of some uniquely identifying components of a process. Constructing a globally unique identifier is a common practice to mitigate PID reuse as well as to identify a specific process over time, across multiple monitored hosts.', + example: 'c2c455d9f99375d', + name: 'process.parent.entity_id', + type: 'keyword', + }, + 'process.parent.executable': { + category: 'process', + description: 'Absolute path to the process executable.', + example: '/usr/bin/ssh', + name: 'process.parent.executable', + type: 'keyword', + }, + 'process.parent.exit_code': { + category: 'process', + description: + 'The exit code of the process, if this is a termination event. The field should be absent if there is no exit code for the event (e.g. process start).', + example: 137, + name: 'process.parent.exit_code', + type: 'long', + }, + 'process.parent.hash.md5': { + category: 'process', + description: 'MD5 hash.', + name: 'process.parent.hash.md5', + type: 'keyword', + }, + 'process.parent.hash.sha1': { + category: 'process', + description: 'SHA1 hash.', + name: 'process.parent.hash.sha1', + type: 'keyword', + }, + 'process.parent.hash.sha256': { + category: 'process', + description: 'SHA256 hash.', + name: 'process.parent.hash.sha256', + type: 'keyword', + }, + 'process.parent.hash.sha512': { + category: 'process', + description: 'SHA512 hash.', + name: 'process.parent.hash.sha512', + type: 'keyword', + }, + 'process.parent.name': { + category: 'process', + description: 'Process name. Sometimes called program name or similar.', + example: 'ssh', + name: 'process.parent.name', + type: 'keyword', + }, + 'process.parent.pgid': { + category: 'process', + description: 'Identifier of the group of processes the process belongs to.', + name: 'process.parent.pgid', + type: 'long', + format: 'string', + }, + 'process.parent.pid': { + category: 'process', + description: 'Process id.', + example: 4242, + name: 'process.parent.pid', + type: 'long', + format: 'string', + }, + 'process.parent.ppid': { + category: 'process', + description: "Parent process' pid.", + example: 4241, + name: 'process.parent.ppid', + type: 'long', + format: 'string', + }, + 'process.parent.start': { + category: 'process', + description: 'The time the process started.', + example: '2016-05-23T08:05:34.853Z', + name: 'process.parent.start', + type: 'date', + }, + 'process.parent.thread.id': { + category: 'process', + description: 'Thread ID.', + example: 4242, + name: 'process.parent.thread.id', + type: 'long', + format: 'string', + }, + 'process.parent.thread.name': { + category: 'process', + description: 'Thread name.', + example: 'thread-0', + name: 'process.parent.thread.name', + type: 'keyword', + }, + 'process.parent.title': { + category: 'process', + description: + 'Process title. The proctitle, some times the same as process name. Can also be different: for example a browser setting its title to the web page currently opened.', + name: 'process.parent.title', + type: 'keyword', + }, + 'process.parent.uptime': { + category: 'process', + description: 'Seconds the process has been up.', + example: 1325, + name: 'process.parent.uptime', + type: 'long', + }, + 'process.parent.working_directory': { + category: 'process', + description: 'The working directory of the process.', + example: '/home/alice', + name: 'process.parent.working_directory', + type: 'keyword', + }, + 'process.pe.company': { + category: 'process', + description: 'Internal company name of the file, provided at compile-time.', + example: 'Microsoft Corporation', + name: 'process.pe.company', + type: 'keyword', + }, + 'process.pe.description': { + category: 'process', + description: 'Internal description of the file, provided at compile-time.', + example: 'Paint', + name: 'process.pe.description', + type: 'keyword', + }, + 'process.pe.file_version': { + category: 'process', + description: 'Internal version of the file, provided at compile-time.', + example: '6.3.9600.17415', + name: 'process.pe.file_version', + type: 'keyword', + }, + 'process.pe.original_file_name': { + category: 'process', + description: 'Internal name of the file, provided at compile-time.', + example: 'MSPAINT.EXE', + name: 'process.pe.original_file_name', + type: 'keyword', + }, + 'process.pe.product': { + category: 'process', + description: 'Internal product name of the file, provided at compile-time.', + example: 'Microsoft® Windows® Operating System', + name: 'process.pe.product', + type: 'keyword', + }, + 'process.pgid': { + category: 'process', + description: 'Identifier of the group of processes the process belongs to.', + name: 'process.pgid', + type: 'long', + format: 'string', + }, + 'process.pid': { + category: 'process', + description: 'Process id.', + example: 4242, + name: 'process.pid', + type: 'long', + format: 'string', + }, + 'process.ppid': { + category: 'process', + description: "Parent process' pid.", + example: 4241, + name: 'process.ppid', + type: 'long', + format: 'string', + }, + 'process.start': { + category: 'process', + description: 'The time the process started.', + example: '2016-05-23T08:05:34.853Z', + name: 'process.start', + type: 'date', + }, + 'process.thread.id': { + category: 'process', + description: 'Thread ID.', + example: 4242, + name: 'process.thread.id', + type: 'long', + format: 'string', + }, + 'process.thread.name': { + category: 'process', + description: 'Thread name.', + example: 'thread-0', + name: 'process.thread.name', + type: 'keyword', + }, + 'process.title': { + category: 'process', + description: + 'Process title. The proctitle, some times the same as process name. Can also be different: for example a browser setting its title to the web page currently opened.', + name: 'process.title', + type: 'keyword', + }, + 'process.uptime': { + category: 'process', + description: 'Seconds the process has been up.', + example: 1325, + name: 'process.uptime', + type: 'long', + }, + 'process.working_directory': { + category: 'process', + description: 'The working directory of the process.', + example: '/home/alice', + name: 'process.working_directory', + type: 'keyword', + }, + 'registry.data.bytes': { + category: 'registry', + description: + 'Original bytes written with base64 encoding. For Windows registry operations, such as SetValueEx and RegQueryValueEx, this corresponds to the data pointed by `lp_data`. This is optional but provides better recoverability and should be populated for REG_BINARY encoded values.', + example: 'ZQBuAC0AVQBTAAAAZQBuAAAAAAA=', + name: 'registry.data.bytes', + type: 'keyword', + }, + 'registry.data.strings': { + category: 'registry', + description: + 'Content when writing string types. Populated as an array when writing string data to the registry. For single string registry types (REG_SZ, REG_EXPAND_SZ), this should be an array with one string. For sequences of string with REG_MULTI_SZ, this array will be variable length. For numeric data, such as REG_DWORD and REG_QWORD, this should be populated with the decimal representation (e.g `"1"`).', + example: '["C:\\rta\\red_ttp\\bin\\myapp.exe"]', + name: 'registry.data.strings', + type: 'keyword', + }, + 'registry.data.type': { + category: 'registry', + description: 'Standard registry type for encoding contents', + example: 'REG_SZ', + name: 'registry.data.type', + type: 'keyword', + }, + 'registry.hive': { + category: 'registry', + description: 'Abbreviated name for the hive.', + example: 'HKLM', + name: 'registry.hive', + type: 'keyword', + }, + 'registry.key': { + category: 'registry', + description: 'Hive-relative path of keys.', + example: + 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\winword.exe', + name: 'registry.key', + type: 'keyword', + }, + 'registry.path': { + category: 'registry', + description: 'Full path, including hive, key and value', + example: + 'HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\winword.exe\\Debugger', + name: 'registry.path', + type: 'keyword', + }, + 'registry.value': { + category: 'registry', + description: 'Name of the value written.', + example: 'Debugger', + name: 'registry.value', + type: 'keyword', + }, + 'related.hash': { + category: 'related', + description: + "All the hashes seen on your event. Populating this field, then using it to search for hashes can help in situations where you're unsure what the hash algorithm is (and therefore which key name to search).", + name: 'related.hash', + type: 'keyword', + }, + 'related.ip': { + category: 'related', + description: 'All of the IPs seen on your event.', + name: 'related.ip', + type: 'ip', + }, + 'related.user': { + category: 'related', + description: 'All the user names seen on your event.', + name: 'related.user', + type: 'keyword', + }, + 'rule.author': { + category: 'rule', + description: + 'Name, organization, or pseudonym of the author or authors who created the rule used to generate this event.', + example: '["Star-Lord"]', + name: 'rule.author', + type: 'keyword', + }, + 'rule.category': { + category: 'rule', + description: + 'A categorization value keyword used by the entity using the rule for detection of this event.', + example: 'Attempted Information Leak', + name: 'rule.category', + type: 'keyword', + }, + 'rule.description': { + category: 'rule', + description: 'The description of the rule generating the event.', + example: 'Block requests to public DNS over HTTPS / TLS protocols', + name: 'rule.description', + type: 'keyword', + }, + 'rule.id': { + category: 'rule', + description: + 'A rule ID that is unique within the scope of an agent, observer, or other entity using the rule for detection of this event.', + example: 101, + name: 'rule.id', + type: 'keyword', + }, + 'rule.license': { + category: 'rule', + description: + 'Name of the license under which the rule used to generate this event is made available.', + example: 'Apache 2.0', + name: 'rule.license', + type: 'keyword', + }, + 'rule.name': { + category: 'rule', + description: 'The name of the rule or signature generating the event.', + example: 'BLOCK_DNS_over_TLS', + name: 'rule.name', + type: 'keyword', + }, + 'rule.reference': { + category: 'rule', + description: + "Reference URL to additional information about the rule used to generate this event. The URL can point to the vendor's documentation about the rule. If that's not available, it can also be a link to a more general page describing this type of alert.", + example: 'https://en.wikipedia.org/wiki/DNS_over_TLS', + name: 'rule.reference', + type: 'keyword', + }, + 'rule.ruleset': { + category: 'rule', + description: + 'Name of the ruleset, policy, group, or parent category in which the rule used to generate this event is a member.', + example: 'Standard_Protocol_Filters', + name: 'rule.ruleset', + type: 'keyword', + }, + 'rule.uuid': { + category: 'rule', + description: + 'A rule ID that is unique within the scope of a set or group of agents, observers, or other entities using the rule for detection of this event.', + example: 1100110011, + name: 'rule.uuid', + type: 'keyword', + }, + 'rule.version': { + category: 'rule', + description: 'The version / revision of the rule being used for analysis.', + example: 1.1, + name: 'rule.version', + type: 'keyword', + }, + 'server.address': { + category: 'server', + description: + 'Some event server addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + name: 'server.address', + type: 'keyword', + }, + 'server.as.number': { + category: 'server', + description: + 'Unique number allocated to the autonomous system. The autonomous system number (ASN) uniquely identifies each network on the Internet.', + example: 15169, + name: 'server.as.number', + type: 'long', + }, + 'server.as.organization.name': { + category: 'server', + description: 'Organization name.', + example: 'Google LLC', + name: 'server.as.organization.name', + type: 'keyword', + }, + 'server.bytes': { + category: 'server', + description: 'Bytes sent from the server to the client.', + example: 184, + name: 'server.bytes', + type: 'long', + format: 'bytes', + }, + 'server.domain': { + category: 'server', + description: 'Server domain.', + name: 'server.domain', + type: 'keyword', + }, + 'server.geo.city_name': { + category: 'server', + description: 'City name.', + example: 'Montreal', + name: 'server.geo.city_name', + type: 'keyword', + }, + 'server.geo.continent_name': { + category: 'server', + description: 'Name of the continent.', + example: 'North America', + name: 'server.geo.continent_name', + type: 'keyword', + }, + 'server.geo.country_iso_code': { + category: 'server', + description: 'Country ISO code.', + example: 'CA', + name: 'server.geo.country_iso_code', + type: 'keyword', + }, + 'server.geo.country_name': { + category: 'server', + description: 'Country name.', + example: 'Canada', + name: 'server.geo.country_name', + type: 'keyword', + }, + 'server.geo.location': { + category: 'server', + description: 'Longitude and latitude.', + example: '{ "lon": -73.614830, "lat": 45.505918 }', + name: 'server.geo.location', + type: 'geo_point', + }, + 'server.geo.name': { + category: 'server', + description: + 'User-defined description of a location, at the level of granularity they care about. Could be the name of their data centers, the floor number, if this describes a local physical entity, city names. Not typically used in automated geolocation.', + example: 'boston-dc', + name: 'server.geo.name', + type: 'keyword', + }, + 'server.geo.region_iso_code': { + category: 'server', + description: 'Region ISO code.', + example: 'CA-QC', + name: 'server.geo.region_iso_code', + type: 'keyword', + }, + 'server.geo.region_name': { + category: 'server', + description: 'Region name.', + example: 'Quebec', + name: 'server.geo.region_name', + type: 'keyword', + }, + 'server.ip': { + category: 'server', + description: 'IP address of the server. Can be one or multiple IPv4 or IPv6 addresses.', + name: 'server.ip', + type: 'ip', + }, + 'server.mac': { + category: 'server', + description: 'MAC address of the server.', + name: 'server.mac', + type: 'keyword', + }, + 'server.nat.ip': { + category: 'server', + description: + 'Translated ip of destination based NAT sessions (e.g. internet to private DMZ) Typically used with load balancers, firewalls, or routers.', + name: 'server.nat.ip', + type: 'ip', + }, + 'server.nat.port': { + category: 'server', + description: + 'Translated port of destination based NAT sessions (e.g. internet to private DMZ) Typically used with load balancers, firewalls, or routers.', + name: 'server.nat.port', + type: 'long', + format: 'string', + }, + 'server.packets': { + category: 'server', + description: 'Packets sent from the server to the client.', + example: 12, + name: 'server.packets', + type: 'long', + }, + 'server.port': { + category: 'server', + description: 'Port of the server.', + name: 'server.port', + type: 'long', + format: 'string', + }, + 'server.registered_domain': { + category: 'server', + description: + 'The highest registered server domain, stripped of the subdomain. For example, the registered domain for "foo.google.com" is "google.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', + example: 'google.com', + name: 'server.registered_domain', + type: 'keyword', + }, + 'server.top_level_domain': { + category: 'server', + description: + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for google.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', + example: 'co.uk', + name: 'server.top_level_domain', + type: 'keyword', + }, + 'server.user.domain': { + category: 'server', + description: + 'Name of the directory the user is a member of. For example, an LDAP or Active Directory domain name.', + name: 'server.user.domain', + type: 'keyword', + }, + 'server.user.email': { + category: 'server', + description: 'User email address.', + name: 'server.user.email', + type: 'keyword', + }, + 'server.user.full_name': { + category: 'server', + description: "User's full name, if available.", + example: 'Albert Einstein', + name: 'server.user.full_name', + type: 'keyword', + }, + 'server.user.group.domain': { + category: 'server', + description: + 'Name of the directory the group is a member of. For example, an LDAP or Active Directory domain name.', + name: 'server.user.group.domain', + type: 'keyword', + }, + 'server.user.group.id': { + category: 'server', + description: 'Unique identifier for the group on the system/platform.', + name: 'server.user.group.id', + type: 'keyword', + }, + 'server.user.group.name': { + category: 'server', + description: 'Name of the group.', + name: 'server.user.group.name', + type: 'keyword', + }, + 'server.user.hash': { + category: 'server', + description: + 'Unique user hash to correlate information for a user in anonymized form. Useful if `user.id` or `user.name` contain confidential information and cannot be used.', + name: 'server.user.hash', + type: 'keyword', + }, + 'server.user.id': { + category: 'server', + description: 'Unique identifiers of the user.', + name: 'server.user.id', + type: 'keyword', + }, + 'server.user.name': { + category: 'server', + description: 'Short name or login of the user.', + example: 'albert', + name: 'server.user.name', + type: 'keyword', + }, + 'service.ephemeral_id': { + category: 'service', + description: + 'Ephemeral identifier of this service (if one exists). This id normally changes across restarts, but `service.id` does not.', + example: '8a4f500f', + name: 'service.ephemeral_id', + type: 'keyword', + }, + 'service.id': { + category: 'service', + description: + 'Unique identifier of the running service. If the service is comprised of many nodes, the `service.id` should be the same for all nodes. This id should uniquely identify the service. This makes it possible to correlate logs and metrics for one specific service, no matter which particular node emitted the event. Note that if you need to see the events from one specific host of the service, you should filter on that `host.name` or `host.id` instead.', + example: 'd37e5ebfe0ae6c4972dbe9f0174a1637bb8247f6', + name: 'service.id', + type: 'keyword', + }, + 'service.name': { + category: 'service', + description: + 'Name of the service data is collected from. The name of the service is normally user given. This allows for distributed services that run on multiple hosts to correlate the related instances based on the name. In the case of Elasticsearch the `service.name` could contain the cluster name. For Beats the `service.name` is by default a copy of the `service.type` field if no name is specified.', + example: 'elasticsearch-metrics', + name: 'service.name', + type: 'keyword', + }, + 'service.node.name': { + category: 'service', + description: + "Name of a service node. This allows for two nodes of the same service running on the same host to be differentiated. Therefore, `service.node.name` should typically be unique across nodes of a given service. In the case of Elasticsearch, the `service.node.name` could contain the unique node name within the Elasticsearch cluster. In cases where the service doesn't have the concept of a node name, the host name or container name can be used to distinguish running instances that make up this service. If those do not provide uniqueness (e.g. multiple instances of the service running on the same host) - the node name can be manually set.", + example: 'instance-0000000016', + name: 'service.node.name', + type: 'keyword', + }, + 'service.state': { + category: 'service', + description: 'Current state of the service.', + name: 'service.state', + type: 'keyword', + }, + 'service.type': { + category: 'service', + description: + 'The type of the service data is collected from. The type can be used to group and correlate logs and metrics from one service type. Example: If logs or metrics are collected from Elasticsearch, `service.type` would be `elasticsearch`.', + example: 'elasticsearch', + name: 'service.type', + type: 'keyword', + }, + 'service.version': { + category: 'service', + description: + 'Version of the service the data was collected from. This allows to look at a data set only for a specific version of a service.', + example: '3.2.4', + name: 'service.version', + type: 'keyword', + }, + 'source.address': { + category: 'source', + description: + 'Some event source addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + name: 'source.address', + type: 'keyword', + }, + 'source.as.number': { + category: 'source', + description: + 'Unique number allocated to the autonomous system. The autonomous system number (ASN) uniquely identifies each network on the Internet.', + example: 15169, + name: 'source.as.number', + type: 'long', + }, + 'source.as.organization.name': { + category: 'source', + description: 'Organization name.', + example: 'Google LLC', + name: 'source.as.organization.name', + type: 'keyword', + }, + 'source.bytes': { + category: 'source', + description: 'Bytes sent from the source to the destination.', + example: 184, + name: 'source.bytes', + type: 'long', + format: 'bytes', + }, + 'source.domain': { + category: 'source', + description: 'Source domain.', + name: 'source.domain', + type: 'keyword', + }, + 'source.geo.city_name': { + category: 'source', + description: 'City name.', + example: 'Montreal', + name: 'source.geo.city_name', + type: 'keyword', + }, + 'source.geo.continent_name': { + category: 'source', + description: 'Name of the continent.', + example: 'North America', + name: 'source.geo.continent_name', + type: 'keyword', + }, + 'source.geo.country_iso_code': { + category: 'source', + description: 'Country ISO code.', + example: 'CA', + name: 'source.geo.country_iso_code', + type: 'keyword', + }, + 'source.geo.country_name': { + category: 'source', + description: 'Country name.', + example: 'Canada', + name: 'source.geo.country_name', + type: 'keyword', + }, + 'source.geo.location': { + category: 'source', + description: 'Longitude and latitude.', + example: '{ "lon": -73.614830, "lat": 45.505918 }', + name: 'source.geo.location', + type: 'geo_point', + }, + 'source.geo.name': { + category: 'source', + description: + 'User-defined description of a location, at the level of granularity they care about. Could be the name of their data centers, the floor number, if this describes a local physical entity, city names. Not typically used in automated geolocation.', + example: 'boston-dc', + name: 'source.geo.name', + type: 'keyword', + }, + 'source.geo.region_iso_code': { + category: 'source', + description: 'Region ISO code.', + example: 'CA-QC', + name: 'source.geo.region_iso_code', + type: 'keyword', + }, + 'source.geo.region_name': { + category: 'source', + description: 'Region name.', + example: 'Quebec', + name: 'source.geo.region_name', + type: 'keyword', + }, + 'source.ip': { + category: 'source', + description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', + name: 'source.ip', + type: 'ip', + }, + 'source.mac': { + category: 'source', + description: 'MAC address of the source.', + name: 'source.mac', + type: 'keyword', + }, + 'source.nat.ip': { + category: 'source', + description: + 'Translated ip of source based NAT sessions (e.g. internal client to internet) Typically connections traversing load balancers, firewalls, or routers.', + name: 'source.nat.ip', + type: 'ip', + }, + 'source.nat.port': { + category: 'source', + description: + 'Translated port of source based NAT sessions. (e.g. internal client to internet) Typically used with load balancers, firewalls, or routers.', + name: 'source.nat.port', + type: 'long', + format: 'string', + }, + 'source.packets': { + category: 'source', + description: 'Packets sent from the source to the destination.', + example: 12, + name: 'source.packets', + type: 'long', + }, + 'source.port': { + category: 'source', + description: 'Port of the source.', + name: 'source.port', + type: 'long', + format: 'string', + }, + 'source.registered_domain': { + category: 'source', + description: + 'The highest registered source domain, stripped of the subdomain. For example, the registered domain for "foo.google.com" is "google.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', + example: 'google.com', + name: 'source.registered_domain', + type: 'keyword', + }, + 'source.top_level_domain': { + category: 'source', + description: + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for google.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', + example: 'co.uk', + name: 'source.top_level_domain', + type: 'keyword', + }, + 'source.user.domain': { + category: 'source', + description: + 'Name of the directory the user is a member of. For example, an LDAP or Active Directory domain name.', + name: 'source.user.domain', + type: 'keyword', + }, + 'source.user.email': { + category: 'source', + description: 'User email address.', + name: 'source.user.email', + type: 'keyword', + }, + 'source.user.full_name': { + category: 'source', + description: "User's full name, if available.", + example: 'Albert Einstein', + name: 'source.user.full_name', + type: 'keyword', + }, + 'source.user.group.domain': { + category: 'source', + description: + 'Name of the directory the group is a member of. For example, an LDAP or Active Directory domain name.', + name: 'source.user.group.domain', + type: 'keyword', + }, + 'source.user.group.id': { + category: 'source', + description: 'Unique identifier for the group on the system/platform.', + name: 'source.user.group.id', + type: 'keyword', + }, + 'source.user.group.name': { + category: 'source', + description: 'Name of the group.', + name: 'source.user.group.name', + type: 'keyword', + }, + 'source.user.hash': { + category: 'source', + description: + 'Unique user hash to correlate information for a user in anonymized form. Useful if `user.id` or `user.name` contain confidential information and cannot be used.', + name: 'source.user.hash', + type: 'keyword', + }, + 'source.user.id': { + category: 'source', + description: 'Unique identifiers of the user.', + name: 'source.user.id', + type: 'keyword', + }, + 'source.user.name': { + category: 'source', + description: 'Short name or login of the user.', + example: 'albert', + name: 'source.user.name', + type: 'keyword', + }, + 'threat.framework': { + category: 'threat', + description: + 'Name of the threat framework used to further categorize and classify the tactic and technique of the reported threat. Framework classification can be provided by detecting systems, evaluated at ingest time, or retrospectively tagged to events.', + example: 'MITRE ATT&CK', + name: 'threat.framework', + type: 'keyword', + }, + 'threat.tactic.id': { + category: 'threat', + description: + 'The id of tactic used by this threat. You can use the Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/ )', + example: 'TA0040', + name: 'threat.tactic.id', + type: 'keyword', + }, + 'threat.tactic.name': { + category: 'threat', + description: + 'Name of the type of tactic used by this threat. You can use the Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/ )', + example: 'impact', + name: 'threat.tactic.name', + type: 'keyword', + }, + 'threat.tactic.reference': { + category: 'threat', + description: + 'The reference url of tactic used by this threat. You can use the Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/tactics/TA0040/ )', + example: 'https://attack.mitre.org/tactics/TA0040/', + name: 'threat.tactic.reference', + type: 'keyword', + }, + 'threat.technique.id': { + category: 'threat', + description: + 'The id of technique used by this tactic. You can use the Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/ )', + example: 'T1499', + name: 'threat.technique.id', + type: 'keyword', + }, + 'threat.technique.name': { + category: 'threat', + description: + 'The name of technique used by this tactic. You can use the Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/ )', + example: 'endpoint denial of service', + name: 'threat.technique.name', + type: 'keyword', + }, + 'threat.technique.reference': { + category: 'threat', + description: + 'The reference url of technique used by this tactic. You can use the Mitre ATT&CK Matrix Tactic categorization, for example. (ex. https://attack.mitre.org/techniques/T1499/ )', + example: 'https://attack.mitre.org/techniques/T1499/', + name: 'threat.technique.reference', + type: 'keyword', + }, + 'tls.cipher': { + category: 'tls', + description: 'String indicating the cipher used during the current connection.', + example: 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256', + name: 'tls.cipher', + type: 'keyword', + }, + 'tls.client.certificate': { + category: 'tls', + description: + 'PEM-encoded stand-alone certificate offered by the client. This is usually mutually-exclusive of `client.certificate_chain` since this value also exists in that list.', + example: 'MII...', + name: 'tls.client.certificate', + type: 'keyword', + }, + 'tls.client.certificate_chain': { + category: 'tls', + description: + 'Array of PEM-encoded certificates that make up the certificate chain offered by the client. This is usually mutually-exclusive of `client.certificate` since that value should be the first certificate in the chain.', + example: '["MII...","MII..."]', + name: 'tls.client.certificate_chain', + type: 'keyword', + }, + 'tls.client.hash.md5': { + category: 'tls', + description: + 'Certificate fingerprint using the MD5 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash.', + example: '0F76C7F2C55BFD7D8E8B8F4BFBF0C9EC', + name: 'tls.client.hash.md5', + type: 'keyword', + }, + 'tls.client.hash.sha1': { + category: 'tls', + description: + 'Certificate fingerprint using the SHA1 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash.', + example: '9E393D93138888D288266C2D915214D1D1CCEB2A', + name: 'tls.client.hash.sha1', + type: 'keyword', + }, + 'tls.client.hash.sha256': { + category: 'tls', + description: + 'Certificate fingerprint using the SHA256 digest of DER-encoded version of certificate offered by the client. For consistency with other hash values, this value should be formatted as an uppercase hash.', + example: '0687F666A054EF17A08E2F2162EAB4CBC0D265E1D7875BE74BF3C712CA92DAF0', + name: 'tls.client.hash.sha256', + type: 'keyword', + }, + 'tls.client.issuer': { + category: 'tls', + description: + 'Distinguished name of subject of the issuer of the x.509 certificate presented by the client.', + example: 'CN=MyDomain Root CA, OU=Infrastructure Team, DC=mydomain, DC=com', + name: 'tls.client.issuer', + type: 'keyword', + }, + 'tls.client.ja3': { + category: 'tls', + description: 'A hash that identifies clients based on how they perform an SSL/TLS handshake.', + example: 'd4e5b18d6b55c71272893221c96ba240', + name: 'tls.client.ja3', + type: 'keyword', + }, + 'tls.client.not_after': { + category: 'tls', + description: 'Date/Time indicating when client certificate is no longer considered valid.', + example: '2021-01-01T00:00:00.000Z', + name: 'tls.client.not_after', + type: 'date', + }, + 'tls.client.not_before': { + category: 'tls', + description: 'Date/Time indicating when client certificate is first considered valid.', + example: '1970-01-01T00:00:00.000Z', + name: 'tls.client.not_before', + type: 'date', + }, + 'tls.client.server_name': { + category: 'tls', + description: + 'Also called an SNI, this tells the server which hostname to which the client is attempting to connect. When this value is available, it should get copied to `destination.domain`.', + example: 'www.elastic.co', + name: 'tls.client.server_name', + type: 'keyword', + }, + 'tls.client.subject': { + category: 'tls', + description: 'Distinguished name of subject of the x.509 certificate presented by the client.', + example: 'CN=myclient, OU=Documentation Team, DC=mydomain, DC=com', + name: 'tls.client.subject', + type: 'keyword', + }, + 'tls.client.supported_ciphers': { + category: 'tls', + description: 'Array of ciphers offered by the client during the client hello.', + example: + '["TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384","..."]', + name: 'tls.client.supported_ciphers', + type: 'keyword', + }, + 'tls.curve': { + category: 'tls', + description: 'String indicating the curve used for the given cipher, when applicable.', + example: 'secp256r1', + name: 'tls.curve', + type: 'keyword', + }, + 'tls.established': { + category: 'tls', + description: + 'Boolean flag indicating if the TLS negotiation was successful and transitioned to an encrypted tunnel.', + name: 'tls.established', + type: 'boolean', + }, + 'tls.next_protocol': { + category: 'tls', + description: + 'String indicating the protocol being tunneled. Per the values in the IANA registry (https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids), this string should be lower case.', + example: 'http/1.1', + name: 'tls.next_protocol', + type: 'keyword', + }, + 'tls.resumed': { + category: 'tls', + description: + 'Boolean flag indicating if this TLS connection was resumed from an existing TLS negotiation.', + name: 'tls.resumed', + type: 'boolean', + }, + 'tls.server.certificate': { + category: 'tls', + description: + 'PEM-encoded stand-alone certificate offered by the server. This is usually mutually-exclusive of `server.certificate_chain` since this value also exists in that list.', + example: 'MII...', + name: 'tls.server.certificate', + type: 'keyword', + }, + 'tls.server.certificate_chain': { + category: 'tls', + description: + 'Array of PEM-encoded certificates that make up the certificate chain offered by the server. This is usually mutually-exclusive of `server.certificate` since that value should be the first certificate in the chain.', + example: '["MII...","MII..."]', + name: 'tls.server.certificate_chain', + type: 'keyword', + }, + 'tls.server.hash.md5': { + category: 'tls', + description: + 'Certificate fingerprint using the MD5 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash.', + example: '0F76C7F2C55BFD7D8E8B8F4BFBF0C9EC', + name: 'tls.server.hash.md5', + type: 'keyword', + }, + 'tls.server.hash.sha1': { + category: 'tls', + description: + 'Certificate fingerprint using the SHA1 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash.', + example: '9E393D93138888D288266C2D915214D1D1CCEB2A', + name: 'tls.server.hash.sha1', + type: 'keyword', + }, + 'tls.server.hash.sha256': { + category: 'tls', + description: + 'Certificate fingerprint using the SHA256 digest of DER-encoded version of certificate offered by the server. For consistency with other hash values, this value should be formatted as an uppercase hash.', + example: '0687F666A054EF17A08E2F2162EAB4CBC0D265E1D7875BE74BF3C712CA92DAF0', + name: 'tls.server.hash.sha256', + type: 'keyword', + }, + 'tls.server.issuer': { + category: 'tls', + description: 'Subject of the issuer of the x.509 certificate presented by the server.', + example: 'CN=MyDomain Root CA, OU=Infrastructure Team, DC=mydomain, DC=com', + name: 'tls.server.issuer', + type: 'keyword', + }, + 'tls.server.ja3s': { + category: 'tls', + description: 'A hash that identifies servers based on how they perform an SSL/TLS handshake.', + example: '394441ab65754e2207b1e1b457b3641d', + name: 'tls.server.ja3s', + type: 'keyword', + }, + 'tls.server.not_after': { + category: 'tls', + description: 'Timestamp indicating when server certificate is no longer considered valid.', + example: '2021-01-01T00:00:00.000Z', + name: 'tls.server.not_after', + type: 'date', + }, + 'tls.server.not_before': { + category: 'tls', + description: 'Timestamp indicating when server certificate is first considered valid.', + example: '1970-01-01T00:00:00.000Z', + name: 'tls.server.not_before', + type: 'date', + }, + 'tls.server.subject': { + category: 'tls', + description: 'Subject of the x.509 certificate presented by the server.', + example: 'CN=www.mydomain.com, OU=Infrastructure Team, DC=mydomain, DC=com', + name: 'tls.server.subject', + type: 'keyword', + }, + 'tls.version': { + category: 'tls', + description: 'Numeric part of the version parsed from the original string.', + example: '1.2', + name: 'tls.version', + type: 'keyword', + }, + 'tls.version_protocol': { + category: 'tls', + description: 'Normalized lowercase protocol name parsed from original string.', + example: 'tls', + name: 'tls.version_protocol', + type: 'keyword', + }, + 'tracing.trace.id': { + category: 'tracing', + description: + 'Unique identifier of the trace. A trace groups multiple events like transactions that belong together. For example, a user request handled by multiple inter-connected services.', + example: '4bf92f3577b34da6a3ce929d0e0e4736', + name: 'tracing.trace.id', + type: 'keyword', + }, + 'tracing.transaction.id': { + category: 'tracing', + description: + 'Unique identifier of the transaction. A transaction is the highest level of work measured within a service, such as a request to a server.', + example: '00f067aa0ba902b7', + name: 'tracing.transaction.id', + type: 'keyword', + }, + 'url.domain': { + category: 'url', + description: + 'Domain of the url, such as "www.elastic.co". In some cases a URL may refer to an IP and/or port directly, without a domain name. In this case, the IP address would go to the `domain` field.', + example: 'www.elastic.co', + name: 'url.domain', + type: 'keyword', + }, + 'url.extension': { + category: 'url', + description: + 'The field contains the file extension from the original request url. The file extension is only set if it exists, as not every url has a file extension. The leading period must not be included. For example, the value must be "png", not ".png".', + example: 'png', + name: 'url.extension', + type: 'keyword', + }, + 'url.fragment': { + category: 'url', + description: + 'Portion of the url after the `#`, such as "top". The `#` is not part of the fragment.', + name: 'url.fragment', + type: 'keyword', + }, + 'url.full': { + category: 'url', + description: + 'If full URLs are important to your use case, they should be stored in `url.full`, whether this field is reconstructed or present in the event source.', + example: 'https://www.elastic.co:443/search?q=elasticsearch#top', + name: 'url.full', + type: 'keyword', + }, + 'url.original': { + category: 'url', + description: + 'Unmodified original url as seen in the event source. Note that in network monitoring, the observed URL may be a full URL, whereas in access logs, the URL is often just represented as a path. This field is meant to represent the URL as it was observed, complete or not.', + example: 'https://www.elastic.co:443/search?q=elasticsearch#top or /search?q=elasticsearch', + name: 'url.original', + type: 'keyword', + }, + 'url.password': { + category: 'url', + description: 'Password of the request.', + name: 'url.password', + type: 'keyword', + }, + 'url.path': { + category: 'url', + description: 'Path of the request, such as "/search".', + name: 'url.path', + type: 'keyword', + }, + 'url.port': { + category: 'url', + description: 'Port of the request, such as 443.', + example: 443, + name: 'url.port', + type: 'long', + format: 'string', + }, + 'url.query': { + category: 'url', + description: + 'The query field describes the query string of the request, such as "q=elasticsearch". The `?` is excluded from the query string. If a URL contains no `?`, there is no query field. If there is a `?` but no query, the query field exists with an empty string. The `exists` query can be used to differentiate between the two cases.', + name: 'url.query', + type: 'keyword', + }, + 'url.registered_domain': { + category: 'url', + description: + 'The highest registered url domain, stripped of the subdomain. For example, the registered domain for "foo.google.com" is "google.com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last two labels will not work well for TLDs such as "co.uk".', + example: 'google.com', + name: 'url.registered_domain', + type: 'keyword', + }, + 'url.scheme': { + category: 'url', + description: 'Scheme of the request, such as "https". Note: The `:` is not part of the scheme.', + example: 'https', + name: 'url.scheme', + type: 'keyword', + }, + 'url.top_level_domain': { + category: 'url', + description: + 'The effective top level domain (eTLD), also known as the domain suffix, is the last part of the domain name. For example, the top level domain for google.com is "com". This value can be determined precisely with a list like the public suffix list (http://publicsuffix.org). Trying to approximate this by simply taking the last label will not work well for effective TLDs such as "co.uk".', + example: 'co.uk', + name: 'url.top_level_domain', + type: 'keyword', + }, + 'url.username': { + category: 'url', + description: 'Username of the request.', + name: 'url.username', + type: 'keyword', + }, + 'user.domain': { + category: 'user', + description: + 'Name of the directory the user is a member of. For example, an LDAP or Active Directory domain name.', + name: 'user.domain', + type: 'keyword', + }, + 'user.email': { + category: 'user', + description: 'User email address.', + name: 'user.email', + type: 'keyword', + }, + 'user.full_name': { + category: 'user', + description: "User's full name, if available.", + example: 'Albert Einstein', + name: 'user.full_name', + type: 'keyword', + }, + 'user.group.domain': { + category: 'user', + description: + 'Name of the directory the group is a member of. For example, an LDAP or Active Directory domain name.', + name: 'user.group.domain', + type: 'keyword', + }, + 'user.group.id': { + category: 'user', + description: 'Unique identifier for the group on the system/platform.', + name: 'user.group.id', + type: 'keyword', + }, + 'user.group.name': { + category: 'user', + description: 'Name of the group.', + name: 'user.group.name', + type: 'keyword', + }, + 'user.hash': { + category: 'user', + description: + 'Unique user hash to correlate information for a user in anonymized form. Useful if `user.id` or `user.name` contain confidential information and cannot be used.', + name: 'user.hash', + type: 'keyword', + }, + 'user.id': { + category: 'user', + description: 'Unique identifiers of the user.', + name: 'user.id', + type: 'keyword', + }, + 'user.name': { + category: 'user', + description: 'Short name or login of the user.', + example: 'albert', + name: 'user.name', + type: 'keyword', + }, + 'user_agent.device.name': { + category: 'user_agent', + description: 'Name of the device.', + example: 'iPhone', + name: 'user_agent.device.name', + type: 'keyword', + }, + 'user_agent.name': { + category: 'user_agent', + description: 'Name of the user agent.', + example: 'Safari', + name: 'user_agent.name', + type: 'keyword', + }, + 'user_agent.original': { + category: 'user_agent', + description: 'Unparsed user_agent string.', + example: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1', + name: 'user_agent.original', + type: 'keyword', + }, + 'user_agent.os.family': { + category: 'user_agent', + description: 'OS family (such as redhat, debian, freebsd, windows).', + example: 'debian', + name: 'user_agent.os.family', + type: 'keyword', + }, + 'user_agent.os.full': { + category: 'user_agent', + description: 'Operating system name, including the version or code name.', + example: 'Mac OS Mojave', + name: 'user_agent.os.full', + type: 'keyword', + }, + 'user_agent.os.kernel': { + category: 'user_agent', + description: 'Operating system kernel version as a raw string.', + example: '4.4.0-112-generic', + name: 'user_agent.os.kernel', + type: 'keyword', + }, + 'user_agent.os.name': { + category: 'user_agent', + description: 'Operating system name, without the version.', + example: 'Mac OS X', + name: 'user_agent.os.name', + type: 'keyword', + }, + 'user_agent.os.platform': { + category: 'user_agent', + description: 'Operating system platform (such centos, ubuntu, windows).', + example: 'darwin', + name: 'user_agent.os.platform', + type: 'keyword', + }, + 'user_agent.os.version': { + category: 'user_agent', + description: 'Operating system version as a raw string.', + example: '10.14.1', + name: 'user_agent.os.version', + type: 'keyword', + }, + 'user_agent.version': { + category: 'user_agent', + description: 'Version of the user agent.', + example: 12, + name: 'user_agent.version', + type: 'keyword', + }, + 'vlan.id': { + category: 'vlan', + description: 'VLAN ID as reported by the observer.', + example: 10, + name: 'vlan.id', + type: 'keyword', + }, + 'vlan.name': { + category: 'vlan', + description: 'Optional VLAN name as reported by the observer.', + example: 'outside', + name: 'vlan.name', + type: 'keyword', + }, + 'vulnerability.category': { + category: 'vulnerability', + description: + 'The type of system or architecture that the vulnerability affects. These may be platform-specific (for example, Debian or SUSE) or general (for example, Database or Firewall). For example (https://qualysguard.qualys.com/qwebhelp/fo_portal/knowledgebase/vulnerability_categories.htm[Qualys vulnerability categories]) This field must be an array.', + example: '["Firewall"]', + name: 'vulnerability.category', + type: 'keyword', + }, + 'vulnerability.classification': { + category: 'vulnerability', + description: + 'The classification of the vulnerability scoring system. For example (https://www.first.org/cvss/)', + example: 'CVSS', + name: 'vulnerability.classification', + type: 'keyword', + }, + 'vulnerability.description': { + category: 'vulnerability', + description: + 'The description of the vulnerability that provides additional context of the vulnerability. For example (https://cve.mitre.org/about/faqs.html#cve_entry_descriptions_created[Common Vulnerabilities and Exposure CVE description])', + example: 'In macOS before 2.12.6, there is a vulnerability in the RPC...', + name: 'vulnerability.description', + type: 'keyword', + }, + 'vulnerability.enumeration': { + category: 'vulnerability', + description: + 'The type of identifier used for this vulnerability. For example (https://cve.mitre.org/about/)', + example: 'CVE', + name: 'vulnerability.enumeration', + type: 'keyword', + }, + 'vulnerability.id': { + category: 'vulnerability', + description: + 'The identification (ID) is the number portion of a vulnerability entry. It includes a unique identification number for the vulnerability. For example (https://cve.mitre.org/about/faqs.html#what_is_cve_id)[Common Vulnerabilities and Exposure CVE ID]', + example: 'CVE-2019-00001', + name: 'vulnerability.id', + type: 'keyword', + }, + 'vulnerability.reference': { + category: 'vulnerability', + description: + 'A resource that provides additional information, context, and mitigations for the identified vulnerability.', + example: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-6111', + name: 'vulnerability.reference', + type: 'keyword', + }, + 'vulnerability.report_id': { + category: 'vulnerability', + description: 'The report or scan identification number.', + example: 20191018.0001, + name: 'vulnerability.report_id', + type: 'keyword', + }, + 'vulnerability.scanner.vendor': { + category: 'vulnerability', + description: 'The name of the vulnerability scanner vendor.', + example: 'Tenable', + name: 'vulnerability.scanner.vendor', + type: 'keyword', + }, + 'vulnerability.score.base': { + category: 'vulnerability', + description: + 'Scores can range from 0.0 to 10.0, with 10.0 being the most severe. Base scores cover an assessment for exploitability metrics (attack vector, complexity, privileges, and user interaction), impact metrics (confidentiality, integrity, and availability), and scope. For example (https://www.first.org/cvss/specification-document)', + example: 5.5, + name: 'vulnerability.score.base', + type: 'float', + }, + 'vulnerability.score.environmental': { + category: 'vulnerability', + description: + 'Scores can range from 0.0 to 10.0, with 10.0 being the most severe. Environmental scores cover an assessment for any modified Base metrics, confidentiality, integrity, and availability requirements. For example (https://www.first.org/cvss/specification-document)', + example: 5.5, + name: 'vulnerability.score.environmental', + type: 'float', + }, + 'vulnerability.score.temporal': { + category: 'vulnerability', + description: + 'Scores can range from 0.0 to 10.0, with 10.0 being the most severe. Temporal scores cover an assessment for code maturity, remediation level, and confidence. For example (https://www.first.org/cvss/specification-document)', + name: 'vulnerability.score.temporal', + type: 'float', + }, + 'vulnerability.score.version': { + category: 'vulnerability', + description: + 'The National Vulnerability Database (NVD) provides qualitative severity rankings of "Low", "Medium", and "High" for CVSS v2.0 base score ranges in addition to the severity ratings for CVSS v3.0 as they are defined in the CVSS v3.0 specification. CVSS is owned and managed by FIRST.Org, Inc. (FIRST), a US-based non-profit organization, whose mission is to help computer security incident response teams across the world. For example (https://nvd.nist.gov/vuln-metrics/cvss)', + example: 2, + name: 'vulnerability.score.version', + type: 'keyword', + }, + 'vulnerability.severity': { + category: 'vulnerability', + description: + 'The severity of the vulnerability can help with metrics and internal prioritization regarding remediation. For example (https://nvd.nist.gov/vuln-metrics/cvss)', + example: 'Critical', + name: 'vulnerability.severity', + type: 'keyword', + }, + 'agent.hostname': { + category: 'agent', + description: + 'Deprecated - use agent.name or agent.id to identify an agent. Hostname of the agent. ', + name: 'agent.hostname', + type: 'keyword', + }, + 'beat.timezone': { + category: 'beat', + name: 'beat.timezone', + type: 'alias', + }, + fields: { + category: 'base', + description: 'Contains user configurable fields. ', + name: 'fields', + type: 'object', + }, + 'beat.name': { + category: 'beat', + name: 'beat.name', + type: 'alias', + }, + 'beat.hostname': { + category: 'beat', + name: 'beat.hostname', + type: 'alias', + }, + 'timeseries.instance': { + category: 'timeseries', + description: 'Time series instance id', + name: 'timeseries.instance', + type: 'keyword', + }, + 'cloud.project.id': { + category: 'cloud', + description: 'Name of the project in Google Cloud. ', + example: 'project-x', + name: 'cloud.project.id', + }, + 'cloud.image.id': { + category: 'cloud', + description: 'Image ID for the cloud instance. ', + example: 'ami-abcd1234', + name: 'cloud.image.id', + }, + 'meta.cloud.provider': { + category: 'meta', + name: 'meta.cloud.provider', + type: 'alias', + }, + 'meta.cloud.instance_id': { + category: 'meta', + name: 'meta.cloud.instance_id', + type: 'alias', + }, + 'meta.cloud.instance_name': { + category: 'meta', + name: 'meta.cloud.instance_name', + type: 'alias', + }, + 'meta.cloud.machine_type': { + category: 'meta', + name: 'meta.cloud.machine_type', + type: 'alias', + }, + 'meta.cloud.availability_zone': { + category: 'meta', + name: 'meta.cloud.availability_zone', + type: 'alias', + }, + 'meta.cloud.project_id': { + category: 'meta', + name: 'meta.cloud.project_id', + type: 'alias', + }, + 'meta.cloud.region': { + category: 'meta', + name: 'meta.cloud.region', + type: 'alias', + }, + 'docker.container.id': { + category: 'docker', + name: 'docker.container.id', + type: 'alias', + }, + 'docker.container.image': { + category: 'docker', + name: 'docker.container.image', + type: 'alias', + }, + 'docker.container.name': { + category: 'docker', + name: 'docker.container.name', + type: 'alias', + }, + 'docker.container.labels': { + category: 'docker', + description: 'Image labels. ', + name: 'docker.container.labels', + type: 'object', + }, + 'host.containerized': { + category: 'host', + description: 'If the host is a container. ', + name: 'host.containerized', + type: 'boolean', + }, + 'host.os.build': { + category: 'host', + description: 'OS build information. ', + example: '18D109', + name: 'host.os.build', + type: 'keyword', + }, + 'host.os.codename': { + category: 'host', + description: 'OS codename, if any. ', + example: 'stretch', + name: 'host.os.codename', + type: 'keyword', + }, + 'kubernetes.pod.name': { + category: 'kubernetes', + description: 'Kubernetes pod name ', + name: 'kubernetes.pod.name', + type: 'keyword', + }, + 'kubernetes.pod.uid': { + category: 'kubernetes', + description: 'Kubernetes Pod UID ', + name: 'kubernetes.pod.uid', + type: 'keyword', + }, + 'kubernetes.namespace': { + category: 'kubernetes', + description: 'Kubernetes namespace ', + name: 'kubernetes.namespace', + type: 'keyword', + }, + 'kubernetes.node.name': { + category: 'kubernetes', + description: 'Kubernetes node name ', + name: 'kubernetes.node.name', + type: 'keyword', + }, + 'kubernetes.labels.*': { + category: 'kubernetes', + description: 'Kubernetes labels map ', + name: 'kubernetes.labels.*', + type: 'object', + }, + 'kubernetes.annotations.*': { + category: 'kubernetes', + description: 'Kubernetes annotations map ', + name: 'kubernetes.annotations.*', + type: 'object', + }, + 'kubernetes.replicaset.name': { + category: 'kubernetes', + description: 'Kubernetes replicaset name ', + name: 'kubernetes.replicaset.name', + type: 'keyword', + }, + 'kubernetes.deployment.name': { + category: 'kubernetes', + description: 'Kubernetes deployment name ', + name: 'kubernetes.deployment.name', + type: 'keyword', + }, + 'kubernetes.statefulset.name': { + category: 'kubernetes', + description: 'Kubernetes statefulset name ', + name: 'kubernetes.statefulset.name', + type: 'keyword', + }, + 'kubernetes.container.name': { + category: 'kubernetes', + description: 'Kubernetes container name ', + name: 'kubernetes.container.name', + type: 'keyword', + }, + 'kubernetes.container.image': { + category: 'kubernetes', + description: 'Kubernetes container image ', + name: 'kubernetes.container.image', + type: 'keyword', + }, + 'process.exe': { + category: 'process', + name: 'process.exe', + type: 'alias', + }, + 'jolokia.agent.version': { + category: 'jolokia', + description: 'Version number of jolokia agent. ', + name: 'jolokia.agent.version', + type: 'keyword', + }, + 'jolokia.agent.id': { + category: 'jolokia', + description: + 'Each agent has a unique id which can be either provided during startup of the agent in form of a configuration parameter or being autodetected. If autodected, the id has several parts: The IP, the process id, hashcode of the agent and its type. ', + name: 'jolokia.agent.id', + type: 'keyword', + }, + 'jolokia.server.product': { + category: 'jolokia', + description: 'The container product if detected. ', + name: 'jolokia.server.product', + type: 'keyword', + }, + 'jolokia.server.version': { + category: 'jolokia', + description: "The container's version (if detected). ", + name: 'jolokia.server.version', + type: 'keyword', + }, + 'jolokia.server.vendor': { + category: 'jolokia', + description: 'The vendor of the container the agent is running in. ', + name: 'jolokia.server.vendor', + type: 'keyword', + }, + 'jolokia.url': { + category: 'jolokia', + description: 'The URL how this agent can be contacted. ', + name: 'jolokia.url', + type: 'keyword', + }, + 'jolokia.secured': { + category: 'jolokia', + description: 'Whether the agent was configured for authentication or not. ', + name: 'jolokia.secured', + type: 'boolean', + }, + 'file.setuid': { + category: 'file', + description: 'Set if the file has the `setuid` bit set. Omitted otherwise.', + example: 'true', + name: 'file.setuid', + type: 'boolean', + }, + 'file.setgid': { + category: 'file', + description: 'Set if the file has the `setgid` bit set. Omitted otherwise.', + example: 'true', + name: 'file.setgid', + type: 'boolean', + }, + 'file.origin': { + category: 'file', + description: + 'An array of strings describing a possible external origin for this file. For example, the URL it was downloaded from. Only supported in macOS, via the kMDItemWhereFroms attribute. Omitted if origin information is not available. ', + name: 'file.origin', + type: 'keyword', + }, + 'file.selinux.user': { + category: 'file', + description: 'The owner of the object.', + name: 'file.selinux.user', + type: 'keyword', + }, + 'file.selinux.role': { + category: 'file', + description: "The object's SELinux role.", + name: 'file.selinux.role', + type: 'keyword', + }, + 'file.selinux.domain': { + category: 'file', + description: "The object's SELinux domain or type.", + name: 'file.selinux.domain', + type: 'keyword', + }, + 'file.selinux.level': { + category: 'file', + description: "The object's SELinux level.", + example: 's0', + name: 'file.selinux.level', + type: 'keyword', + }, + 'user.audit.id': { + category: 'user', + description: 'Audit user ID.', + name: 'user.audit.id', + type: 'keyword', + }, + 'user.audit.name': { + category: 'user', + description: 'Audit user name.', + name: 'user.audit.name', + type: 'keyword', + }, + 'user.effective.id': { + category: 'user', + description: 'Effective user ID.', + name: 'user.effective.id', + type: 'keyword', + }, + 'user.effective.name': { + category: 'user', + description: 'Effective user name.', + name: 'user.effective.name', + type: 'keyword', + }, + 'user.effective.group.id': { + category: 'user', + description: 'Effective group ID.', + name: 'user.effective.group.id', + type: 'keyword', + }, + 'user.effective.group.name': { + category: 'user', + description: 'Effective group name.', + name: 'user.effective.group.name', + type: 'keyword', + }, + 'user.filesystem.id': { + category: 'user', + description: 'Filesystem user ID.', + name: 'user.filesystem.id', + type: 'keyword', + }, + 'user.filesystem.name': { + category: 'user', + description: 'Filesystem user name.', + name: 'user.filesystem.name', + type: 'keyword', + }, + 'user.filesystem.group.id': { + category: 'user', + description: 'Filesystem group ID.', + name: 'user.filesystem.group.id', + type: 'keyword', + }, + 'user.filesystem.group.name': { + category: 'user', + description: 'Filesystem group name.', + name: 'user.filesystem.group.name', + type: 'keyword', + }, + 'user.saved.id': { + category: 'user', + description: 'Saved user ID.', + name: 'user.saved.id', + type: 'keyword', + }, + 'user.saved.name': { + category: 'user', + description: 'Saved user name.', + name: 'user.saved.name', + type: 'keyword', + }, + 'user.saved.group.id': { + category: 'user', + description: 'Saved group ID.', + name: 'user.saved.group.id', + type: 'keyword', + }, + 'user.saved.group.name': { + category: 'user', + description: 'Saved group name.', + name: 'user.saved.group.name', + type: 'keyword', + }, + 'user.auid': { + category: 'user', + name: 'user.auid', + type: 'alias', + }, + 'user.uid': { + category: 'user', + name: 'user.uid', + type: 'alias', + }, + 'user.euid': { + category: 'user', + name: 'user.euid', + type: 'alias', + }, + 'user.fsuid': { + category: 'user', + name: 'user.fsuid', + type: 'alias', + }, + 'user.suid': { + category: 'user', + name: 'user.suid', + type: 'alias', + }, + 'user.gid': { + category: 'user', + name: 'user.gid', + type: 'alias', + }, + 'user.egid': { + category: 'user', + name: 'user.egid', + type: 'alias', + }, + 'user.sgid': { + category: 'user', + name: 'user.sgid', + type: 'alias', + }, + 'user.fsgid': { + category: 'user', + name: 'user.fsgid', + type: 'alias', + }, + 'user.name_map.auid': { + category: 'user', + name: 'user.name_map.auid', + type: 'alias', + }, + 'user.name_map.uid': { + category: 'user', + name: 'user.name_map.uid', + type: 'alias', + }, + 'user.name_map.euid': { + category: 'user', + name: 'user.name_map.euid', + type: 'alias', + }, + 'user.name_map.fsuid': { + category: 'user', + name: 'user.name_map.fsuid', + type: 'alias', + }, + 'user.name_map.suid': { + category: 'user', + name: 'user.name_map.suid', + type: 'alias', + }, + 'user.name_map.gid': { + category: 'user', + name: 'user.name_map.gid', + type: 'alias', + }, + 'user.name_map.egid': { + category: 'user', + name: 'user.name_map.egid', + type: 'alias', + }, + 'user.name_map.sgid': { + category: 'user', + name: 'user.name_map.sgid', + type: 'alias', + }, + 'user.name_map.fsgid': { + category: 'user', + name: 'user.name_map.fsgid', + type: 'alias', + }, + 'user.selinux.user': { + category: 'user', + description: 'account submitted for authentication', + name: 'user.selinux.user', + type: 'keyword', + }, + 'user.selinux.role': { + category: 'user', + description: "user's SELinux role", + name: 'user.selinux.role', + type: 'keyword', + }, + 'user.selinux.domain': { + category: 'user', + description: "The actor's SELinux domain or type.", + name: 'user.selinux.domain', + type: 'keyword', + }, + 'user.selinux.level': { + category: 'user', + description: "The actor's SELinux level.", + example: 's0', + name: 'user.selinux.level', + type: 'keyword', + }, + 'user.selinux.category': { + category: 'user', + description: "The actor's SELinux category or compartments.", + name: 'user.selinux.category', + type: 'keyword', + }, + 'process.cwd': { + category: 'process', + description: 'The current working directory.', + name: 'process.cwd', + type: 'alias', + }, + 'source.path': { + category: 'source', + description: 'This is the path associated with a unix socket.', + name: 'source.path', + type: 'keyword', + }, + 'destination.path': { + category: 'destination', + description: 'This is the path associated with a unix socket.', + name: 'destination.path', + type: 'keyword', + }, + 'auditd.message_type': { + category: 'auditd', + description: 'The audit message type (e.g. syscall or apparmor_denied). ', + example: 'syscall', + name: 'auditd.message_type', + type: 'keyword', + }, + 'auditd.sequence': { + category: 'auditd', + description: + 'The sequence number of the event as assigned by the kernel. Sequence numbers are stored as a uint32 in the kernel and can rollover. ', + name: 'auditd.sequence', + type: 'long', + }, + 'auditd.session': { + category: 'auditd', + description: + 'The session ID assigned to a login. All events related to a login session will have the same value. ', + name: 'auditd.session', + type: 'keyword', + }, + 'auditd.result': { + category: 'auditd', + description: 'The result of the audited operation (success/fail).', + example: 'success or fail', + name: 'auditd.result', + type: 'keyword', + }, + 'auditd.summary.actor.primary': { + category: 'auditd', + description: + "The primary identity of the actor. This is the actor's original login ID. It will not change even if the user changes to another account. ", + name: 'auditd.summary.actor.primary', + type: 'keyword', + }, + 'auditd.summary.actor.secondary': { + category: 'auditd', + description: + 'The secondary identity of the actor. This is typically the same as the primary, except for when the user has used `su`.', + name: 'auditd.summary.actor.secondary', + type: 'keyword', + }, + 'auditd.summary.object.type': { + category: 'auditd', + description: 'A description of the what the "thing" is (e.g. file, socket, user-session). ', + name: 'auditd.summary.object.type', + type: 'keyword', + }, + 'auditd.summary.object.primary': { + category: 'auditd', + description: '', + name: 'auditd.summary.object.primary', + type: 'keyword', + }, + 'auditd.summary.object.secondary': { + category: 'auditd', + description: '', + name: 'auditd.summary.object.secondary', + type: 'keyword', + }, + 'auditd.summary.how': { + category: 'auditd', + description: + 'This describes how the action was performed. Usually this is the exe or command that was being executed that triggered the event. ', + name: 'auditd.summary.how', + type: 'keyword', + }, + 'auditd.paths.inode': { + category: 'auditd', + description: 'inode number', + name: 'auditd.paths.inode', + type: 'keyword', + }, + 'auditd.paths.dev': { + category: 'auditd', + description: 'device name as found in /dev', + name: 'auditd.paths.dev', + type: 'keyword', + }, + 'auditd.paths.obj_user': { + category: 'auditd', + description: '', + name: 'auditd.paths.obj_user', + type: 'keyword', + }, + 'auditd.paths.obj_role': { + category: 'auditd', + description: '', + name: 'auditd.paths.obj_role', + type: 'keyword', + }, + 'auditd.paths.obj_domain': { + category: 'auditd', + description: '', + name: 'auditd.paths.obj_domain', + type: 'keyword', + }, + 'auditd.paths.obj_level': { + category: 'auditd', + description: '', + name: 'auditd.paths.obj_level', + type: 'keyword', + }, + 'auditd.paths.objtype': { + category: 'auditd', + description: '', + name: 'auditd.paths.objtype', + type: 'keyword', + }, + 'auditd.paths.ouid': { + category: 'auditd', + description: 'file owner user ID', + name: 'auditd.paths.ouid', + type: 'keyword', + }, + 'auditd.paths.rdev': { + category: 'auditd', + description: 'the device identifier (special files only)', + name: 'auditd.paths.rdev', + type: 'keyword', + }, + 'auditd.paths.nametype': { + category: 'auditd', + description: 'kind of file operation being referenced', + name: 'auditd.paths.nametype', + type: 'keyword', + }, + 'auditd.paths.ogid': { + category: 'auditd', + description: 'file owner group ID', + name: 'auditd.paths.ogid', + type: 'keyword', + }, + 'auditd.paths.item': { + category: 'auditd', + description: 'which item is being recorded', + name: 'auditd.paths.item', + type: 'keyword', + }, + 'auditd.paths.mode': { + category: 'auditd', + description: 'mode flags on a file', + name: 'auditd.paths.mode', + type: 'keyword', + }, + 'auditd.paths.name': { + category: 'auditd', + description: 'file name in avcs', + name: 'auditd.paths.name', + type: 'keyword', + }, + 'auditd.data.action': { + category: 'auditd', + description: 'netfilter packet disposition', + name: 'auditd.data.action', + type: 'keyword', + }, + 'auditd.data.minor': { + category: 'auditd', + description: 'device minor number', + name: 'auditd.data.minor', + type: 'keyword', + }, + 'auditd.data.acct': { + category: 'auditd', + description: "a user's account name", + name: 'auditd.data.acct', + type: 'keyword', + }, + 'auditd.data.addr': { + category: 'auditd', + description: 'the remote address that the user is connecting from', + name: 'auditd.data.addr', + type: 'keyword', + }, + 'auditd.data.cipher': { + category: 'auditd', + description: 'name of crypto cipher selected', + name: 'auditd.data.cipher', + type: 'keyword', + }, + 'auditd.data.id': { + category: 'auditd', + description: 'during account changes', + name: 'auditd.data.id', + type: 'keyword', + }, + 'auditd.data.entries': { + category: 'auditd', + description: 'number of entries in the netfilter table', + name: 'auditd.data.entries', + type: 'keyword', + }, + 'auditd.data.kind': { + category: 'auditd', + description: 'server or client in crypto operation', + name: 'auditd.data.kind', + type: 'keyword', + }, + 'auditd.data.ksize': { + category: 'auditd', + description: 'key size for crypto operation', + name: 'auditd.data.ksize', + type: 'keyword', + }, + 'auditd.data.spid': { + category: 'auditd', + description: 'sent process ID', + name: 'auditd.data.spid', + type: 'keyword', + }, + 'auditd.data.arch': { + category: 'auditd', + description: 'the elf architecture flags', + name: 'auditd.data.arch', + type: 'keyword', + }, + 'auditd.data.argc': { + category: 'auditd', + description: 'the number of arguments to an execve syscall', + name: 'auditd.data.argc', + type: 'keyword', + }, + 'auditd.data.major': { + category: 'auditd', + description: 'device major number', + name: 'auditd.data.major', + type: 'keyword', + }, + 'auditd.data.unit': { + category: 'auditd', + description: 'systemd unit', + name: 'auditd.data.unit', + type: 'keyword', + }, + 'auditd.data.table': { + category: 'auditd', + description: 'netfilter table name', + name: 'auditd.data.table', + type: 'keyword', + }, + 'auditd.data.terminal': { + category: 'auditd', + description: 'terminal name the user is running programs on', + name: 'auditd.data.terminal', + type: 'keyword', + }, + 'auditd.data.grantors': { + category: 'auditd', + description: 'pam modules approving the action', + name: 'auditd.data.grantors', + type: 'keyword', + }, + 'auditd.data.direction': { + category: 'auditd', + description: 'direction of crypto operation', + name: 'auditd.data.direction', + type: 'keyword', + }, + 'auditd.data.op': { + category: 'auditd', + description: 'the operation being performed that is audited', + name: 'auditd.data.op', + type: 'keyword', + }, + 'auditd.data.tty': { + category: 'auditd', + description: 'tty udevice the user is running programs on', + name: 'auditd.data.tty', + type: 'keyword', + }, + 'auditd.data.syscall': { + category: 'auditd', + description: 'syscall number in effect when the event occurred', + name: 'auditd.data.syscall', + type: 'keyword', + }, + 'auditd.data.data': { + category: 'auditd', + description: 'TTY text', + name: 'auditd.data.data', + type: 'keyword', + }, + 'auditd.data.family': { + category: 'auditd', + description: 'netfilter protocol', + name: 'auditd.data.family', + type: 'keyword', + }, + 'auditd.data.mac': { + category: 'auditd', + description: 'crypto MAC algorithm selected', + name: 'auditd.data.mac', + type: 'keyword', + }, + 'auditd.data.pfs': { + category: 'auditd', + description: 'perfect forward secrecy method', + name: 'auditd.data.pfs', + type: 'keyword', + }, + 'auditd.data.items': { + category: 'auditd', + description: 'the number of path records in the event', + name: 'auditd.data.items', + type: 'keyword', + }, + 'auditd.data.a0': { + category: 'auditd', + description: '', + name: 'auditd.data.a0', + type: 'keyword', + }, + 'auditd.data.a1': { + category: 'auditd', + description: '', + name: 'auditd.data.a1', + type: 'keyword', + }, + 'auditd.data.a2': { + category: 'auditd', + description: '', + name: 'auditd.data.a2', + type: 'keyword', + }, + 'auditd.data.a3': { + category: 'auditd', + description: '', + name: 'auditd.data.a3', + type: 'keyword', + }, + 'auditd.data.hostname': { + category: 'auditd', + description: 'the hostname that the user is connecting from', + name: 'auditd.data.hostname', + type: 'keyword', + }, + 'auditd.data.lport': { + category: 'auditd', + description: 'local network port', + name: 'auditd.data.lport', + type: 'keyword', + }, + 'auditd.data.rport': { + category: 'auditd', + description: 'remote port number', + name: 'auditd.data.rport', + type: 'keyword', + }, + 'auditd.data.exit': { + category: 'auditd', + description: 'syscall exit code', + name: 'auditd.data.exit', + type: 'keyword', + }, + 'auditd.data.fp': { + category: 'auditd', + description: 'crypto key finger print', + name: 'auditd.data.fp', + type: 'keyword', + }, + 'auditd.data.laddr': { + category: 'auditd', + description: 'local network address', + name: 'auditd.data.laddr', + type: 'keyword', + }, + 'auditd.data.sport': { + category: 'auditd', + description: 'local port number', + name: 'auditd.data.sport', + type: 'keyword', + }, + 'auditd.data.capability': { + category: 'auditd', + description: 'posix capabilities', + name: 'auditd.data.capability', + type: 'keyword', + }, + 'auditd.data.nargs': { + category: 'auditd', + description: 'the number of arguments to a socket call', + name: 'auditd.data.nargs', + type: 'keyword', + }, + 'auditd.data.new-enabled': { + category: 'auditd', + description: 'new TTY audit enabled setting', + name: 'auditd.data.new-enabled', + type: 'keyword', + }, + 'auditd.data.audit_backlog_limit': { + category: 'auditd', + description: "audit system's backlog queue size", + name: 'auditd.data.audit_backlog_limit', + type: 'keyword', + }, + 'auditd.data.dir': { + category: 'auditd', + description: 'directory name', + name: 'auditd.data.dir', + type: 'keyword', + }, + 'auditd.data.cap_pe': { + category: 'auditd', + description: 'process effective capability map', + name: 'auditd.data.cap_pe', + type: 'keyword', + }, + 'auditd.data.model': { + category: 'auditd', + description: 'security model being used for virt', + name: 'auditd.data.model', + type: 'keyword', + }, + 'auditd.data.new_pp': { + category: 'auditd', + description: 'new process permitted capability map', + name: 'auditd.data.new_pp', + type: 'keyword', + }, + 'auditd.data.old-enabled': { + category: 'auditd', + description: 'present TTY audit enabled setting', + name: 'auditd.data.old-enabled', + type: 'keyword', + }, + 'auditd.data.oauid': { + category: 'auditd', + description: "object's login user ID", + name: 'auditd.data.oauid', + type: 'keyword', + }, + 'auditd.data.old': { + category: 'auditd', + description: 'old value', + name: 'auditd.data.old', + type: 'keyword', + }, + 'auditd.data.banners': { + category: 'auditd', + description: 'banners used on printed page', + name: 'auditd.data.banners', + type: 'keyword', + }, + 'auditd.data.feature': { + category: 'auditd', + description: 'kernel feature being changed', + name: 'auditd.data.feature', + type: 'keyword', + }, + 'auditd.data.vm-ctx': { + category: 'auditd', + description: "the vm's context string", + name: 'auditd.data.vm-ctx', + type: 'keyword', + }, + 'auditd.data.opid': { + category: 'auditd', + description: "object's process ID", + name: 'auditd.data.opid', + type: 'keyword', + }, + 'auditd.data.seperms': { + category: 'auditd', + description: 'SELinux permissions being used', + name: 'auditd.data.seperms', + type: 'keyword', + }, + 'auditd.data.seresult': { + category: 'auditd', + description: 'SELinux AVC decision granted/denied', + name: 'auditd.data.seresult', + type: 'keyword', + }, + 'auditd.data.new-rng': { + category: 'auditd', + description: 'device name of rng being added from a vm', + name: 'auditd.data.new-rng', + type: 'keyword', + }, + 'auditd.data.old-net': { + category: 'auditd', + description: 'present MAC address assigned to vm', + name: 'auditd.data.old-net', + type: 'keyword', + }, + 'auditd.data.sigev_signo': { + category: 'auditd', + description: 'signal number', + name: 'auditd.data.sigev_signo', + type: 'keyword', + }, + 'auditd.data.ino': { + category: 'auditd', + description: 'inode number', + name: 'auditd.data.ino', + type: 'keyword', + }, + 'auditd.data.old_enforcing': { + category: 'auditd', + description: 'old MAC enforcement status', + name: 'auditd.data.old_enforcing', + type: 'keyword', + }, + 'auditd.data.old-vcpu': { + category: 'auditd', + description: 'present number of CPU cores', + name: 'auditd.data.old-vcpu', + type: 'keyword', + }, + 'auditd.data.range': { + category: 'auditd', + description: "user's SE Linux range", + name: 'auditd.data.range', + type: 'keyword', + }, + 'auditd.data.res': { + category: 'auditd', + description: 'result of the audited operation(success/fail)', + name: 'auditd.data.res', + type: 'keyword', + }, + 'auditd.data.added': { + category: 'auditd', + description: 'number of new files detected', + name: 'auditd.data.added', + type: 'keyword', + }, + 'auditd.data.fam': { + category: 'auditd', + description: 'socket address family', + name: 'auditd.data.fam', + type: 'keyword', + }, + 'auditd.data.nlnk-pid': { + category: 'auditd', + description: 'pid of netlink packet sender', + name: 'auditd.data.nlnk-pid', + type: 'keyword', + }, + 'auditd.data.subj': { + category: 'auditd', + description: "lspp subject's context string", + name: 'auditd.data.subj', + type: 'keyword', + }, + 'auditd.data.a[0-3]': { + category: 'auditd', + description: 'the arguments to a syscall', + name: 'auditd.data.a[0-3]', + type: 'keyword', + }, + 'auditd.data.cgroup': { + category: 'auditd', + description: 'path to cgroup in sysfs', + name: 'auditd.data.cgroup', + type: 'keyword', + }, + 'auditd.data.kernel': { + category: 'auditd', + description: "kernel's version number", + name: 'auditd.data.kernel', + type: 'keyword', + }, + 'auditd.data.ocomm': { + category: 'auditd', + description: "object's command line name", + name: 'auditd.data.ocomm', + type: 'keyword', + }, + 'auditd.data.new-net': { + category: 'auditd', + description: 'MAC address being assigned to vm', + name: 'auditd.data.new-net', + type: 'keyword', + }, + 'auditd.data.permissive': { + category: 'auditd', + description: 'SELinux is in permissive mode', + name: 'auditd.data.permissive', + type: 'keyword', + }, + 'auditd.data.class': { + category: 'auditd', + description: 'resource class assigned to vm', + name: 'auditd.data.class', + type: 'keyword', + }, + 'auditd.data.compat': { + category: 'auditd', + description: 'is_compat_task result', + name: 'auditd.data.compat', + type: 'keyword', + }, + 'auditd.data.fi': { + category: 'auditd', + description: 'file assigned inherited capability map', + name: 'auditd.data.fi', + type: 'keyword', + }, + 'auditd.data.changed': { + category: 'auditd', + description: 'number of changed files', + name: 'auditd.data.changed', + type: 'keyword', + }, + 'auditd.data.msg': { + category: 'auditd', + description: 'the payload of the audit record', + name: 'auditd.data.msg', + type: 'keyword', + }, + 'auditd.data.dport': { + category: 'auditd', + description: 'remote port number', + name: 'auditd.data.dport', + type: 'keyword', + }, + 'auditd.data.new-seuser': { + category: 'auditd', + description: 'new SELinux user', + name: 'auditd.data.new-seuser', + type: 'keyword', + }, + 'auditd.data.invalid_context': { + category: 'auditd', + description: 'SELinux context', + name: 'auditd.data.invalid_context', + type: 'keyword', + }, + 'auditd.data.dmac': { + category: 'auditd', + description: 'remote MAC address', + name: 'auditd.data.dmac', + type: 'keyword', + }, + 'auditd.data.ipx-net': { + category: 'auditd', + description: 'IPX network number', + name: 'auditd.data.ipx-net', + type: 'keyword', + }, + 'auditd.data.iuid': { + category: 'auditd', + description: "ipc object's user ID", + name: 'auditd.data.iuid', + type: 'keyword', + }, + 'auditd.data.macproto': { + category: 'auditd', + description: 'ethernet packet type ID field', + name: 'auditd.data.macproto', + type: 'keyword', + }, + 'auditd.data.obj': { + category: 'auditd', + description: 'lspp object context string', + name: 'auditd.data.obj', + type: 'keyword', + }, + 'auditd.data.ipid': { + category: 'auditd', + description: 'IP datagram fragment identifier', + name: 'auditd.data.ipid', + type: 'keyword', + }, + 'auditd.data.new-fs': { + category: 'auditd', + description: 'file system being added to vm', + name: 'auditd.data.new-fs', + type: 'keyword', + }, + 'auditd.data.vm-pid': { + category: 'auditd', + description: "vm's process ID", + name: 'auditd.data.vm-pid', + type: 'keyword', + }, + 'auditd.data.cap_pi': { + category: 'auditd', + description: 'process inherited capability map', + name: 'auditd.data.cap_pi', + type: 'keyword', + }, + 'auditd.data.old-auid': { + category: 'auditd', + description: 'previous auid value', + name: 'auditd.data.old-auid', + type: 'keyword', + }, + 'auditd.data.oses': { + category: 'auditd', + description: "object's session ID", + name: 'auditd.data.oses', + type: 'keyword', + }, + 'auditd.data.fd': { + category: 'auditd', + description: 'file descriptor number', + name: 'auditd.data.fd', + type: 'keyword', + }, + 'auditd.data.igid': { + category: 'auditd', + description: "ipc object's group ID", + name: 'auditd.data.igid', + type: 'keyword', + }, + 'auditd.data.new-disk': { + category: 'auditd', + description: 'disk being added to vm', + name: 'auditd.data.new-disk', + type: 'keyword', + }, + 'auditd.data.parent': { + category: 'auditd', + description: 'the inode number of the parent file', + name: 'auditd.data.parent', + type: 'keyword', + }, + 'auditd.data.len': { + category: 'auditd', + description: 'length', + name: 'auditd.data.len', + type: 'keyword', + }, + 'auditd.data.oflag': { + category: 'auditd', + description: 'open syscall flags', + name: 'auditd.data.oflag', + type: 'keyword', + }, + 'auditd.data.uuid': { + category: 'auditd', + description: 'a UUID', + name: 'auditd.data.uuid', + type: 'keyword', + }, + 'auditd.data.code': { + category: 'auditd', + description: 'seccomp action code', + name: 'auditd.data.code', + type: 'keyword', + }, + 'auditd.data.nlnk-grp': { + category: 'auditd', + description: 'netlink group number', + name: 'auditd.data.nlnk-grp', + type: 'keyword', + }, + 'auditd.data.cap_fp': { + category: 'auditd', + description: 'file permitted capability map', + name: 'auditd.data.cap_fp', + type: 'keyword', + }, + 'auditd.data.new-mem': { + category: 'auditd', + description: 'new amount of memory in KB', + name: 'auditd.data.new-mem', + type: 'keyword', + }, + 'auditd.data.seperm': { + category: 'auditd', + description: 'SELinux permission being decided on', + name: 'auditd.data.seperm', + type: 'keyword', + }, + 'auditd.data.enforcing': { + category: 'auditd', + description: 'new MAC enforcement status', + name: 'auditd.data.enforcing', + type: 'keyword', + }, + 'auditd.data.new-chardev': { + category: 'auditd', + description: 'new character device being assigned to vm', + name: 'auditd.data.new-chardev', + type: 'keyword', + }, + 'auditd.data.old-rng': { + category: 'auditd', + description: 'device name of rng being removed from a vm', + name: 'auditd.data.old-rng', + type: 'keyword', + }, + 'auditd.data.outif': { + category: 'auditd', + description: 'out interface number', + name: 'auditd.data.outif', + type: 'keyword', + }, + 'auditd.data.cmd': { + category: 'auditd', + description: 'command being executed', + name: 'auditd.data.cmd', + type: 'keyword', + }, + 'auditd.data.hook': { + category: 'auditd', + description: 'netfilter hook that packet came from', + name: 'auditd.data.hook', + type: 'keyword', + }, + 'auditd.data.new-level': { + category: 'auditd', + description: 'new run level', + name: 'auditd.data.new-level', + type: 'keyword', + }, + 'auditd.data.sauid': { + category: 'auditd', + description: 'sent login user ID', + name: 'auditd.data.sauid', + type: 'keyword', + }, + 'auditd.data.sig': { + category: 'auditd', + description: 'signal number', + name: 'auditd.data.sig', + type: 'keyword', + }, + 'auditd.data.audit_backlog_wait_time': { + category: 'auditd', + description: "audit system's backlog wait time", + name: 'auditd.data.audit_backlog_wait_time', + type: 'keyword', + }, + 'auditd.data.printer': { + category: 'auditd', + description: 'printer name', + name: 'auditd.data.printer', + type: 'keyword', + }, + 'auditd.data.old-mem': { + category: 'auditd', + description: 'present amount of memory in KB', + name: 'auditd.data.old-mem', + type: 'keyword', + }, + 'auditd.data.perm': { + category: 'auditd', + description: 'the file permission being used', + name: 'auditd.data.perm', + type: 'keyword', + }, + 'auditd.data.old_pi': { + category: 'auditd', + description: 'old process inherited capability map', + name: 'auditd.data.old_pi', + type: 'keyword', + }, + 'auditd.data.state': { + category: 'auditd', + description: 'audit daemon configuration resulting state', + name: 'auditd.data.state', + type: 'keyword', + }, + 'auditd.data.format': { + category: 'auditd', + description: "audit log's format", + name: 'auditd.data.format', + type: 'keyword', + }, + 'auditd.data.new_gid': { + category: 'auditd', + description: 'new group ID being assigned', + name: 'auditd.data.new_gid', + type: 'keyword', + }, + 'auditd.data.tcontext': { + category: 'auditd', + description: "the target's or object's context string", + name: 'auditd.data.tcontext', + type: 'keyword', + }, + 'auditd.data.maj': { + category: 'auditd', + description: 'device major number', + name: 'auditd.data.maj', + type: 'keyword', + }, + 'auditd.data.watch': { + category: 'auditd', + description: 'file name in a watch record', + name: 'auditd.data.watch', + type: 'keyword', + }, + 'auditd.data.device': { + category: 'auditd', + description: 'device name', + name: 'auditd.data.device', + type: 'keyword', + }, + 'auditd.data.grp': { + category: 'auditd', + description: 'group name', + name: 'auditd.data.grp', + type: 'keyword', + }, + 'auditd.data.bool': { + category: 'auditd', + description: 'name of SELinux boolean', + name: 'auditd.data.bool', + type: 'keyword', + }, + 'auditd.data.icmp_type': { + category: 'auditd', + description: 'type of icmp message', + name: 'auditd.data.icmp_type', + type: 'keyword', + }, + 'auditd.data.new_lock': { + category: 'auditd', + description: 'new value of feature lock', + name: 'auditd.data.new_lock', + type: 'keyword', + }, + 'auditd.data.old_prom': { + category: 'auditd', + description: 'network promiscuity flag', + name: 'auditd.data.old_prom', + type: 'keyword', + }, + 'auditd.data.acl': { + category: 'auditd', + description: 'access mode of resource assigned to vm', + name: 'auditd.data.acl', + type: 'keyword', + }, + 'auditd.data.ip': { + category: 'auditd', + description: 'network address of a printer', + name: 'auditd.data.ip', + type: 'keyword', + }, + 'auditd.data.new_pi': { + category: 'auditd', + description: 'new process inherited capability map', + name: 'auditd.data.new_pi', + type: 'keyword', + }, + 'auditd.data.default-context': { + category: 'auditd', + description: 'default MAC context', + name: 'auditd.data.default-context', + type: 'keyword', + }, + 'auditd.data.inode_gid': { + category: 'auditd', + description: "group ID of the inode's owner", + name: 'auditd.data.inode_gid', + type: 'keyword', + }, + 'auditd.data.new-log_passwd': { + category: 'auditd', + description: 'new value for TTY password logging', + name: 'auditd.data.new-log_passwd', + type: 'keyword', + }, + 'auditd.data.new_pe': { + category: 'auditd', + description: 'new process effective capability map', + name: 'auditd.data.new_pe', + type: 'keyword', + }, + 'auditd.data.selected-context': { + category: 'auditd', + description: 'new MAC context assigned to session', + name: 'auditd.data.selected-context', + type: 'keyword', + }, + 'auditd.data.cap_fver': { + category: 'auditd', + description: 'file system capabilities version number', + name: 'auditd.data.cap_fver', + type: 'keyword', + }, + 'auditd.data.file': { + category: 'auditd', + description: 'file name', + name: 'auditd.data.file', + type: 'keyword', + }, + 'auditd.data.net': { + category: 'auditd', + description: 'network MAC address', + name: 'auditd.data.net', + type: 'keyword', + }, + 'auditd.data.virt': { + category: 'auditd', + description: 'kind of virtualization being referenced', + name: 'auditd.data.virt', + type: 'keyword', + }, + 'auditd.data.cap_pp': { + category: 'auditd', + description: 'process permitted capability map', + name: 'auditd.data.cap_pp', + type: 'keyword', + }, + 'auditd.data.old-range': { + category: 'auditd', + description: 'present SELinux range', + name: 'auditd.data.old-range', + type: 'keyword', + }, + 'auditd.data.resrc': { + category: 'auditd', + description: 'resource being assigned', + name: 'auditd.data.resrc', + type: 'keyword', + }, + 'auditd.data.new-range': { + category: 'auditd', + description: 'new SELinux range', + name: 'auditd.data.new-range', + type: 'keyword', + }, + 'auditd.data.obj_gid': { + category: 'auditd', + description: 'group ID of object', + name: 'auditd.data.obj_gid', + type: 'keyword', + }, + 'auditd.data.proto': { + category: 'auditd', + description: 'network protocol', + name: 'auditd.data.proto', + type: 'keyword', + }, + 'auditd.data.old-disk': { + category: 'auditd', + description: 'disk being removed from vm', + name: 'auditd.data.old-disk', + type: 'keyword', + }, + 'auditd.data.audit_failure': { + category: 'auditd', + description: "audit system's failure mode", + name: 'auditd.data.audit_failure', + type: 'keyword', + }, + 'auditd.data.inif': { + category: 'auditd', + description: 'in interface number', + name: 'auditd.data.inif', + type: 'keyword', + }, + 'auditd.data.vm': { + category: 'auditd', + description: 'virtual machine name', + name: 'auditd.data.vm', + type: 'keyword', + }, + 'auditd.data.flags': { + category: 'auditd', + description: 'mmap syscall flags', + name: 'auditd.data.flags', + type: 'keyword', + }, + 'auditd.data.nlnk-fam': { + category: 'auditd', + description: 'netlink protocol number', + name: 'auditd.data.nlnk-fam', + type: 'keyword', + }, + 'auditd.data.old-fs': { + category: 'auditd', + description: 'file system being removed from vm', + name: 'auditd.data.old-fs', + type: 'keyword', + }, + 'auditd.data.old-ses': { + category: 'auditd', + description: 'previous ses value', + name: 'auditd.data.old-ses', + type: 'keyword', + }, + 'auditd.data.seqno': { + category: 'auditd', + description: 'sequence number', + name: 'auditd.data.seqno', + type: 'keyword', + }, + 'auditd.data.fver': { + category: 'auditd', + description: 'file system capabilities version number', + name: 'auditd.data.fver', + type: 'keyword', + }, + 'auditd.data.qbytes': { + category: 'auditd', + description: 'ipc objects quantity of bytes', + name: 'auditd.data.qbytes', + type: 'keyword', + }, + 'auditd.data.seuser': { + category: 'auditd', + description: "user's SE Linux user acct", + name: 'auditd.data.seuser', + type: 'keyword', + }, + 'auditd.data.cap_fe': { + category: 'auditd', + description: 'file assigned effective capability map', + name: 'auditd.data.cap_fe', + type: 'keyword', + }, + 'auditd.data.new-vcpu': { + category: 'auditd', + description: 'new number of CPU cores', + name: 'auditd.data.new-vcpu', + type: 'keyword', + }, + 'auditd.data.old-level': { + category: 'auditd', + description: 'old run level', + name: 'auditd.data.old-level', + type: 'keyword', + }, + 'auditd.data.old_pp': { + category: 'auditd', + description: 'old process permitted capability map', + name: 'auditd.data.old_pp', + type: 'keyword', + }, + 'auditd.data.daddr': { + category: 'auditd', + description: 'remote IP address', + name: 'auditd.data.daddr', + type: 'keyword', + }, + 'auditd.data.old-role': { + category: 'auditd', + description: 'present SELinux role', + name: 'auditd.data.old-role', + type: 'keyword', + }, + 'auditd.data.ioctlcmd': { + category: 'auditd', + description: 'The request argument to the ioctl syscall', + name: 'auditd.data.ioctlcmd', + type: 'keyword', + }, + 'auditd.data.smac': { + category: 'auditd', + description: 'local MAC address', + name: 'auditd.data.smac', + type: 'keyword', + }, + 'auditd.data.apparmor': { + category: 'auditd', + description: 'apparmor event information', + name: 'auditd.data.apparmor', + type: 'keyword', + }, + 'auditd.data.fe': { + category: 'auditd', + description: 'file assigned effective capability map', + name: 'auditd.data.fe', + type: 'keyword', + }, + 'auditd.data.perm_mask': { + category: 'auditd', + description: 'file permission mask that triggered a watch event', + name: 'auditd.data.perm_mask', + type: 'keyword', + }, + 'auditd.data.ses': { + category: 'auditd', + description: 'login session ID', + name: 'auditd.data.ses', + type: 'keyword', + }, + 'auditd.data.cap_fi': { + category: 'auditd', + description: 'file inherited capability map', + name: 'auditd.data.cap_fi', + type: 'keyword', + }, + 'auditd.data.obj_uid': { + category: 'auditd', + description: 'user ID of object', + name: 'auditd.data.obj_uid', + type: 'keyword', + }, + 'auditd.data.reason': { + category: 'auditd', + description: 'text string denoting a reason for the action', + name: 'auditd.data.reason', + type: 'keyword', + }, + 'auditd.data.list': { + category: 'auditd', + description: "the audit system's filter list number", + name: 'auditd.data.list', + type: 'keyword', + }, + 'auditd.data.old_lock': { + category: 'auditd', + description: 'present value of feature lock', + name: 'auditd.data.old_lock', + type: 'keyword', + }, + 'auditd.data.bus': { + category: 'auditd', + description: 'name of subsystem bus a vm resource belongs to', + name: 'auditd.data.bus', + type: 'keyword', + }, + 'auditd.data.old_pe': { + category: 'auditd', + description: 'old process effective capability map', + name: 'auditd.data.old_pe', + type: 'keyword', + }, + 'auditd.data.new-role': { + category: 'auditd', + description: 'new SELinux role', + name: 'auditd.data.new-role', + type: 'keyword', + }, + 'auditd.data.prom': { + category: 'auditd', + description: 'network promiscuity flag', + name: 'auditd.data.prom', + type: 'keyword', + }, + 'auditd.data.uri': { + category: 'auditd', + description: 'URI pointing to a printer', + name: 'auditd.data.uri', + type: 'keyword', + }, + 'auditd.data.audit_enabled': { + category: 'auditd', + description: "audit systems's enable/disable status", + name: 'auditd.data.audit_enabled', + type: 'keyword', + }, + 'auditd.data.old-log_passwd': { + category: 'auditd', + description: 'present value for TTY password logging', + name: 'auditd.data.old-log_passwd', + type: 'keyword', + }, + 'auditd.data.old-seuser': { + category: 'auditd', + description: 'present SELinux user', + name: 'auditd.data.old-seuser', + type: 'keyword', + }, + 'auditd.data.per': { + category: 'auditd', + description: 'linux personality', + name: 'auditd.data.per', + type: 'keyword', + }, + 'auditd.data.scontext': { + category: 'auditd', + description: "the subject's context string", + name: 'auditd.data.scontext', + type: 'keyword', + }, + 'auditd.data.tclass': { + category: 'auditd', + description: "target's object classification", + name: 'auditd.data.tclass', + type: 'keyword', + }, + 'auditd.data.ver': { + category: 'auditd', + description: "audit daemon's version number", + name: 'auditd.data.ver', + type: 'keyword', + }, + 'auditd.data.new': { + category: 'auditd', + description: 'value being set in feature', + name: 'auditd.data.new', + type: 'keyword', + }, + 'auditd.data.val': { + category: 'auditd', + description: 'generic value associated with the operation', + name: 'auditd.data.val', + type: 'keyword', + }, + 'auditd.data.img-ctx': { + category: 'auditd', + description: "the vm's disk image context string", + name: 'auditd.data.img-ctx', + type: 'keyword', + }, + 'auditd.data.old-chardev': { + category: 'auditd', + description: 'present character device assigned to vm', + name: 'auditd.data.old-chardev', + type: 'keyword', + }, + 'auditd.data.old_val': { + category: 'auditd', + description: 'current value of SELinux boolean', + name: 'auditd.data.old_val', + type: 'keyword', + }, + 'auditd.data.success': { + category: 'auditd', + description: 'whether the syscall was successful or not', + name: 'auditd.data.success', + type: 'keyword', + }, + 'auditd.data.inode_uid': { + category: 'auditd', + description: "user ID of the inode's owner", + name: 'auditd.data.inode_uid', + type: 'keyword', + }, + 'auditd.data.removed': { + category: 'auditd', + description: 'number of deleted files', + name: 'auditd.data.removed', + type: 'keyword', + }, + 'auditd.data.socket.port': { + category: 'auditd', + description: 'The port number.', + name: 'auditd.data.socket.port', + type: 'keyword', + }, + 'auditd.data.socket.saddr': { + category: 'auditd', + description: 'The raw socket address structure.', + name: 'auditd.data.socket.saddr', + type: 'keyword', + }, + 'auditd.data.socket.addr': { + category: 'auditd', + description: 'The remote address.', + name: 'auditd.data.socket.addr', + type: 'keyword', + }, + 'auditd.data.socket.family': { + category: 'auditd', + description: 'The socket family (unix, ipv4, ipv6, netlink).', + example: 'unix', + name: 'auditd.data.socket.family', + type: 'keyword', + }, + 'auditd.data.socket.path': { + category: 'auditd', + description: 'This is the path associated with a unix socket.', + name: 'auditd.data.socket.path', + type: 'keyword', + }, + 'auditd.messages': { + category: 'auditd', + description: + 'An ordered list of the raw messages received from the kernel that were used to construct this document. This field is present if an error occurred processing the data or if `include_raw_message` is set in the config. ', + name: 'auditd.messages', + type: 'alias', + }, + 'auditd.warnings': { + category: 'auditd', + description: + 'The warnings generated by the Beat during the construction of the event. These are disabled by default and are used for development and debug purposes only. ', + name: 'auditd.warnings', + type: 'alias', + }, + 'geoip.continent_name': { + category: 'geoip', + description: 'The name of the continent. ', + name: 'geoip.continent_name', + type: 'keyword', + }, + 'geoip.city_name': { + category: 'geoip', + description: 'The name of the city. ', + name: 'geoip.city_name', + type: 'keyword', + }, + 'geoip.region_name': { + category: 'geoip', + description: 'The name of the region. ', + name: 'geoip.region_name', + type: 'keyword', + }, + 'geoip.country_iso_code': { + category: 'geoip', + description: 'Country ISO code. ', + name: 'geoip.country_iso_code', + type: 'keyword', + }, + 'geoip.location': { + category: 'geoip', + description: 'The longitude and latitude. ', + name: 'geoip.location', + type: 'geo_point', + }, + 'hash.blake2b_256': { + category: 'hash', + description: 'BLAKE2b-256 hash of the file.', + name: 'hash.blake2b_256', + type: 'keyword', + }, + 'hash.blake2b_384': { + category: 'hash', + description: 'BLAKE2b-384 hash of the file.', + name: 'hash.blake2b_384', + type: 'keyword', + }, + 'hash.blake2b_512': { + category: 'hash', + description: 'BLAKE2b-512 hash of the file.', + name: 'hash.blake2b_512', + type: 'keyword', + }, + 'hash.sha224': { + category: 'hash', + description: 'SHA224 hash of the file.', + name: 'hash.sha224', + type: 'keyword', + }, + 'hash.sha384': { + category: 'hash', + description: 'SHA384 hash of the file.', + name: 'hash.sha384', + type: 'keyword', + }, + 'hash.sha3_224': { + category: 'hash', + description: 'SHA3_224 hash of the file.', + name: 'hash.sha3_224', + type: 'keyword', + }, + 'hash.sha3_256': { + category: 'hash', + description: 'SHA3_256 hash of the file.', + name: 'hash.sha3_256', + type: 'keyword', + }, + 'hash.sha3_384': { + category: 'hash', + description: 'SHA3_384 hash of the file.', + name: 'hash.sha3_384', + type: 'keyword', + }, + 'hash.sha3_512': { + category: 'hash', + description: 'SHA3_512 hash of the file.', + name: 'hash.sha3_512', + type: 'keyword', + }, + 'hash.sha512_224': { + category: 'hash', + description: 'SHA512/224 hash of the file.', + name: 'hash.sha512_224', + type: 'keyword', + }, + 'hash.sha512_256': { + category: 'hash', + description: 'SHA512/256 hash of the file.', + name: 'hash.sha512_256', + type: 'keyword', + }, + 'hash.xxh64': { + category: 'hash', + description: 'XX64 hash of the file.', + name: 'hash.xxh64', + type: 'keyword', + }, + 'event.origin': { + category: 'event', + description: + 'Origin of the event. This can be a file path (e.g. `/var/log/log.1`), or the name of the system component that supplied the data (e.g. `netlink`). ', + name: 'event.origin', + type: 'keyword', + }, + 'user.entity_id': { + category: 'user', + description: + 'ID uniquely identifying the user on a host. It is computed as a SHA-256 hash of the host ID, user ID, and user name. ', + name: 'user.entity_id', + type: 'keyword', + }, + 'user.terminal': { + category: 'user', + description: 'Terminal of the user. ', + name: 'user.terminal', + type: 'keyword', + }, + 'process.hash.blake2b_256': { + category: 'process', + description: 'BLAKE2b-256 hash of the executable.', + name: 'process.hash.blake2b_256', + type: 'keyword', + }, + 'process.hash.blake2b_384': { + category: 'process', + description: 'BLAKE2b-384 hash of the executable.', + name: 'process.hash.blake2b_384', + type: 'keyword', + }, + 'process.hash.blake2b_512': { + category: 'process', + description: 'BLAKE2b-512 hash of the executable.', + name: 'process.hash.blake2b_512', + type: 'keyword', + }, + 'process.hash.sha224': { + category: 'process', + description: 'SHA224 hash of the executable.', + name: 'process.hash.sha224', + type: 'keyword', + }, + 'process.hash.sha384': { + category: 'process', + description: 'SHA384 hash of the executable.', + name: 'process.hash.sha384', + type: 'keyword', + }, + 'process.hash.sha3_224': { + category: 'process', + description: 'SHA3_224 hash of the executable.', + name: 'process.hash.sha3_224', + type: 'keyword', + }, + 'process.hash.sha3_256': { + category: 'process', + description: 'SHA3_256 hash of the executable.', + name: 'process.hash.sha3_256', + type: 'keyword', + }, + 'process.hash.sha3_384': { + category: 'process', + description: 'SHA3_384 hash of the executable.', + name: 'process.hash.sha3_384', + type: 'keyword', + }, + 'process.hash.sha3_512': { + category: 'process', + description: 'SHA3_512 hash of the executable.', + name: 'process.hash.sha3_512', + type: 'keyword', + }, + 'process.hash.sha512_224': { + category: 'process', + description: 'SHA512/224 hash of the executable.', + name: 'process.hash.sha512_224', + type: 'keyword', + }, + 'process.hash.sha512_256': { + category: 'process', + description: 'SHA512/256 hash of the executable.', + name: 'process.hash.sha512_256', + type: 'keyword', + }, + 'process.hash.xxh64': { + category: 'process', + description: 'XX64 hash of the executable.', + name: 'process.hash.xxh64', + type: 'keyword', + }, + 'socket.entity_id': { + category: 'socket', + description: + 'ID uniquely identifying the socket. It is computed as a SHA-256 hash of the host ID, socket inode, local IP, local port, remote IP, and remote port. ', + name: 'socket.entity_id', + type: 'keyword', + }, + 'system.audit.host.uptime': { + category: 'system', + description: 'Uptime in nanoseconds. ', + name: 'system.audit.host.uptime', + type: 'long', + format: 'duration', + }, + 'system.audit.host.boottime': { + category: 'system', + description: 'Boot time. ', + name: 'system.audit.host.boottime', + type: 'date', + }, + 'system.audit.host.containerized': { + category: 'system', + description: 'Set if host is a container. ', + name: 'system.audit.host.containerized', + type: 'boolean', + }, + 'system.audit.host.timezone.name': { + category: 'system', + description: 'Name of the timezone of the host, e.g. BST. ', + name: 'system.audit.host.timezone.name', + type: 'keyword', + }, + 'system.audit.host.timezone.offset.sec': { + category: 'system', + description: 'Timezone offset in seconds. ', + name: 'system.audit.host.timezone.offset.sec', + type: 'long', + }, + 'system.audit.host.hostname': { + category: 'system', + description: 'Hostname. ', + name: 'system.audit.host.hostname', + type: 'keyword', + }, + 'system.audit.host.id': { + category: 'system', + description: 'Host ID. ', + name: 'system.audit.host.id', + type: 'keyword', + }, + 'system.audit.host.architecture': { + category: 'system', + description: 'Host architecture (e.g. x86_64). ', + name: 'system.audit.host.architecture', + type: 'keyword', + }, + 'system.audit.host.mac': { + category: 'system', + description: 'MAC addresses. ', + name: 'system.audit.host.mac', + type: 'keyword', + }, + 'system.audit.host.ip': { + category: 'system', + description: 'IP addresses. ', + name: 'system.audit.host.ip', + type: 'ip', + }, + 'system.audit.host.os.codename': { + category: 'system', + description: 'OS codename, if any (e.g. stretch). ', + name: 'system.audit.host.os.codename', + type: 'keyword', + }, + 'system.audit.host.os.platform': { + category: 'system', + description: 'OS platform (e.g. centos, ubuntu, windows). ', + name: 'system.audit.host.os.platform', + type: 'keyword', + }, + 'system.audit.host.os.name': { + category: 'system', + description: 'OS name (e.g. Mac OS X). ', + name: 'system.audit.host.os.name', + type: 'keyword', + }, + 'system.audit.host.os.family': { + category: 'system', + description: 'OS family (e.g. redhat, debian, freebsd, windows). ', + name: 'system.audit.host.os.family', + type: 'keyword', + }, + 'system.audit.host.os.version': { + category: 'system', + description: 'OS version. ', + name: 'system.audit.host.os.version', + type: 'keyword', + }, + 'system.audit.host.os.kernel': { + category: 'system', + description: "The operating system's kernel version. ", + name: 'system.audit.host.os.kernel', + type: 'keyword', + }, + 'system.audit.package.entity_id': { + category: 'system', + description: + 'ID uniquely identifying the package. It is computed as a SHA-256 hash of the host ID, package name, and package version. ', + name: 'system.audit.package.entity_id', + type: 'keyword', + }, + 'system.audit.package.name': { + category: 'system', + description: 'Package name. ', + name: 'system.audit.package.name', + type: 'keyword', + }, + 'system.audit.package.version': { + category: 'system', + description: 'Package version. ', + name: 'system.audit.package.version', + type: 'keyword', + }, + 'system.audit.package.release': { + category: 'system', + description: 'Package release. ', + name: 'system.audit.package.release', + type: 'keyword', + }, + 'system.audit.package.arch': { + category: 'system', + description: 'Package architecture. ', + name: 'system.audit.package.arch', + type: 'keyword', + }, + 'system.audit.package.license': { + category: 'system', + description: 'Package license. ', + name: 'system.audit.package.license', + type: 'keyword', + }, + 'system.audit.package.installtime': { + category: 'system', + description: 'Package install time. ', + name: 'system.audit.package.installtime', + type: 'date', + }, + 'system.audit.package.size': { + category: 'system', + description: 'Package size. ', + name: 'system.audit.package.size', + type: 'long', + }, + 'system.audit.package.summary': { + category: 'system', + description: 'Package summary. ', + name: 'system.audit.package.summary', + }, + 'system.audit.package.url': { + category: 'system', + description: 'Package URL. ', + name: 'system.audit.package.url', + type: 'keyword', + }, + 'system.audit.user.name': { + category: 'system', + description: 'User name. ', + name: 'system.audit.user.name', + type: 'keyword', + }, + 'system.audit.user.uid': { + category: 'system', + description: 'User ID. ', + name: 'system.audit.user.uid', + type: 'keyword', + }, + 'system.audit.user.gid': { + category: 'system', + description: 'Group ID. ', + name: 'system.audit.user.gid', + type: 'keyword', + }, + 'system.audit.user.dir': { + category: 'system', + description: "User's home directory. ", + name: 'system.audit.user.dir', + type: 'keyword', + }, + 'system.audit.user.shell': { + category: 'system', + description: 'Program to run at login. ', + name: 'system.audit.user.shell', + type: 'keyword', + }, + 'system.audit.user.user_information': { + category: 'system', + description: 'General user information. On Linux, this is the gecos field. ', + name: 'system.audit.user.user_information', + type: 'keyword', + }, + 'system.audit.user.group.name': { + category: 'system', + description: 'Group name. ', + name: 'system.audit.user.group.name', + type: 'keyword', + }, + 'system.audit.user.group.gid': { + category: 'system', + description: 'Group ID. ', + name: 'system.audit.user.group.gid', + type: 'integer', + }, + 'system.audit.user.password.type': { + category: 'system', + description: + "A user's password type. Possible values are `shadow_password` (the password hash is in the shadow file), `password_disabled`, `no_password` (this is dangerous as anyone can log in), and `crypt_password` (when the password field in /etc/passwd seems to contain an encrypted password). ", + name: 'system.audit.user.password.type', + type: 'keyword', + }, + 'system.audit.user.password.last_changed': { + category: 'system', + description: "The day the user's password was last changed. ", + name: 'system.audit.user.password.last_changed', + type: 'date', + }, + 'log.file.path': { + category: 'log', + description: + 'The file from which the line was read. This field contains the absolute path to the file. For example: `/var/log/system.log`. ', + name: 'log.file.path', + type: 'keyword', + }, + 'log.source.address': { + category: 'log', + description: 'Source address from which the log event was read / sent from. ', + name: 'log.source.address', + type: 'keyword', + }, + 'log.offset': { + category: 'log', + description: 'The file offset the reported line starts at. ', + name: 'log.offset', + type: 'long', + }, + stream: { + category: 'base', + description: "Log stream when reading container logs, can be 'stdout' or 'stderr' ", + name: 'stream', + type: 'keyword', + }, + 'input.type': { + category: 'input', + description: + 'The input type from which the event was generated. This field is set to the value specified for the `type` option in the input section of the Filebeat config file. ', + name: 'input.type', + }, + 'syslog.facility': { + category: 'syslog', + description: 'The facility extracted from the priority. ', + name: 'syslog.facility', + type: 'long', + }, + 'syslog.priority': { + category: 'syslog', + description: 'The priority of the syslog event. ', + name: 'syslog.priority', + type: 'long', + }, + 'syslog.severity_label': { + category: 'syslog', + description: 'The human readable severity. ', + name: 'syslog.severity_label', + type: 'keyword', + }, + 'syslog.facility_label': { + category: 'syslog', + description: 'The human readable facility. ', + name: 'syslog.facility_label', + type: 'keyword', + }, + 'process.program': { + category: 'process', + description: 'The name of the program. ', + name: 'process.program', + type: 'keyword', + }, + 'log.flags': { + category: 'log', + description: 'This field contains the flags of the event. ', + name: 'log.flags', + }, + 'http.response.content_length': { + category: 'http', + name: 'http.response.content_length', + type: 'alias', + }, + 'user_agent.os.full_name': { + category: 'user_agent', + name: 'user_agent.os.full_name', + type: 'keyword', + }, + 'fileset.name': { + category: 'fileset', + description: 'The Filebeat fileset that generated this event. ', + name: 'fileset.name', + type: 'keyword', + }, + 'fileset.module': { + category: 'fileset', + name: 'fileset.module', + type: 'alias', + }, + read_timestamp: { + category: 'base', + name: 'read_timestamp', + type: 'alias', + }, + 'docker.attrs': { + category: 'docker', + description: + "docker.attrs contains labels and environment variables written by docker's JSON File logging driver. These fields are only available when they are configured in the logging driver options. ", + name: 'docker.attrs', + type: 'object', + }, + 'icmp.code': { + category: 'icmp', + description: 'ICMP code. ', + name: 'icmp.code', + type: 'keyword', + }, + 'icmp.type': { + category: 'icmp', + description: 'ICMP type. ', + name: 'icmp.type', + type: 'keyword', + }, + 'igmp.type': { + category: 'igmp', + description: 'IGMP type. ', + name: 'igmp.type', + type: 'keyword', + }, + 'azure.eventhub': { + category: 'azure', + description: 'Name of the eventhub. ', + name: 'azure.eventhub', + type: 'keyword', + }, + 'azure.offset': { + category: 'azure', + description: 'The offset. ', + name: 'azure.offset', + type: 'long', + }, + 'azure.enqueued_time': { + category: 'azure', + description: 'The enqueued time. ', + name: 'azure.enqueued_time', + type: 'date', + }, + 'azure.partition_id': { + category: 'azure', + description: 'The partition id. ', + name: 'azure.partition_id', + type: 'long', + }, + 'azure.consumer_group': { + category: 'azure', + description: 'The consumer group. ', + name: 'azure.consumer_group', + type: 'keyword', + }, + 'azure.sequence_number': { + category: 'azure', + description: 'The sequence number. ', + name: 'azure.sequence_number', + type: 'long', + }, + 'kafka.topic': { + category: 'kafka', + description: 'Kafka topic ', + name: 'kafka.topic', + type: 'keyword', + }, + 'kafka.partition': { + category: 'kafka', + description: 'Kafka partition number ', + name: 'kafka.partition', + type: 'long', + }, + 'kafka.offset': { + category: 'kafka', + description: 'Kafka offset of this message ', + name: 'kafka.offset', + type: 'long', + }, + 'kafka.key': { + category: 'kafka', + description: 'Kafka key, corresponding to the Kafka value stored in the message ', + name: 'kafka.key', + type: 'keyword', + }, + 'kafka.block_timestamp': { + category: 'kafka', + description: 'Kafka outer (compressed) block timestamp ', + name: 'kafka.block_timestamp', + type: 'date', + }, + 'kafka.headers': { + category: 'kafka', + description: + 'An array of Kafka header strings for this message, in the form ": ". ', + name: 'kafka.headers', + type: 'array', + }, + 'apache2.access.remote_ip': { + category: 'apache2', + name: 'apache2.access.remote_ip', + type: 'alias', + }, + 'apache2.access.ssl.protocol': { + category: 'apache2', + name: 'apache2.access.ssl.protocol', + type: 'alias', + }, + 'apache2.access.ssl.cipher': { + category: 'apache2', + name: 'apache2.access.ssl.cipher', + type: 'alias', + }, + 'apache2.access.body_sent.bytes': { + category: 'apache2', + name: 'apache2.access.body_sent.bytes', + type: 'alias', + }, + 'apache2.access.user_name': { + category: 'apache2', + name: 'apache2.access.user_name', + type: 'alias', + }, + 'apache2.access.method': { + category: 'apache2', + name: 'apache2.access.method', + type: 'alias', + }, + 'apache2.access.url': { + category: 'apache2', + name: 'apache2.access.url', + type: 'alias', + }, + 'apache2.access.http_version': { + category: 'apache2', + name: 'apache2.access.http_version', + type: 'alias', + }, + 'apache2.access.response_code': { + category: 'apache2', + name: 'apache2.access.response_code', + type: 'alias', + }, + 'apache2.access.referrer': { + category: 'apache2', + name: 'apache2.access.referrer', + type: 'alias', + }, + 'apache2.access.agent': { + category: 'apache2', + name: 'apache2.access.agent', + type: 'alias', + }, + 'apache2.access.user_agent.device': { + category: 'apache2', + name: 'apache2.access.user_agent.device', + type: 'alias', + }, + 'apache2.access.user_agent.name': { + category: 'apache2', + name: 'apache2.access.user_agent.name', + type: 'alias', + }, + 'apache2.access.user_agent.os': { + category: 'apache2', + name: 'apache2.access.user_agent.os', + type: 'alias', + }, + 'apache2.access.user_agent.os_name': { + category: 'apache2', + name: 'apache2.access.user_agent.os_name', + type: 'alias', + }, + 'apache2.access.user_agent.original': { + category: 'apache2', + name: 'apache2.access.user_agent.original', + type: 'alias', + }, + 'apache2.access.geoip.continent_name': { + category: 'apache2', + name: 'apache2.access.geoip.continent_name', + type: 'alias', + }, + 'apache2.access.geoip.country_iso_code': { + category: 'apache2', + name: 'apache2.access.geoip.country_iso_code', + type: 'alias', + }, + 'apache2.access.geoip.location': { + category: 'apache2', + name: 'apache2.access.geoip.location', + type: 'alias', + }, + 'apache2.access.geoip.region_name': { + category: 'apache2', + name: 'apache2.access.geoip.region_name', + type: 'alias', + }, + 'apache2.access.geoip.city_name': { + category: 'apache2', + name: 'apache2.access.geoip.city_name', + type: 'alias', + }, + 'apache2.access.geoip.region_iso_code': { + category: 'apache2', + name: 'apache2.access.geoip.region_iso_code', + type: 'alias', + }, + 'apache2.error.level': { + category: 'apache2', + name: 'apache2.error.level', + type: 'alias', + }, + 'apache2.error.message': { + category: 'apache2', + name: 'apache2.error.message', + type: 'alias', + }, + 'apache2.error.pid': { + category: 'apache2', + name: 'apache2.error.pid', + type: 'alias', + }, + 'apache2.error.tid': { + category: 'apache2', + name: 'apache2.error.tid', + type: 'alias', + }, + 'apache2.error.module': { + category: 'apache2', + name: 'apache2.error.module', + type: 'alias', + }, + 'apache.access.ssl.protocol': { + category: 'apache', + description: 'SSL protocol version. ', + name: 'apache.access.ssl.protocol', + type: 'keyword', + }, + 'apache.access.ssl.cipher': { + category: 'apache', + description: 'SSL cipher name. ', + name: 'apache.access.ssl.cipher', + type: 'keyword', + }, + 'apache.error.module': { + category: 'apache', + description: 'The module producing the logged message. ', + name: 'apache.error.module', + type: 'keyword', + }, + 'user.audit.group.id': { + category: 'user', + description: 'Unique identifier for the group on the system/platform. ', + name: 'user.audit.group.id', + type: 'keyword', + }, + 'user.audit.group.name': { + category: 'user', + description: 'Name of the group. ', + name: 'user.audit.group.name', + type: 'keyword', + }, + 'user.owner.id': { + category: 'user', + description: 'One or multiple unique identifiers of the user. ', + name: 'user.owner.id', + type: 'keyword', + }, + 'user.owner.name': { + category: 'user', + description: 'Short name or login of the user. ', + example: 'albert', + name: 'user.owner.name', + type: 'keyword', + }, + 'user.owner.group.id': { + category: 'user', + description: 'Unique identifier for the group on the system/platform. ', + name: 'user.owner.group.id', + type: 'keyword', + }, + 'user.owner.group.name': { + category: 'user', + description: 'Name of the group. ', + name: 'user.owner.group.name', + type: 'keyword', + }, + 'auditd.log.old_auid': { + category: 'auditd', + description: + 'For login events this is the old audit ID used for the user prior to this login. ', + name: 'auditd.log.old_auid', + }, + 'auditd.log.new_auid': { + category: 'auditd', + description: + 'For login events this is the new audit ID. The audit ID can be used to trace future events to the user even if their identity changes (like becoming root). ', + name: 'auditd.log.new_auid', + }, + 'auditd.log.old_ses': { + category: 'auditd', + description: + 'For login events this is the old session ID used for the user prior to this login. ', + name: 'auditd.log.old_ses', + }, + 'auditd.log.new_ses': { + category: 'auditd', + description: + 'For login events this is the new session ID. It can be used to tie a user to future events by session ID. ', + name: 'auditd.log.new_ses', + }, + 'auditd.log.sequence': { + category: 'auditd', + description: 'The audit event sequence number. ', + name: 'auditd.log.sequence', + type: 'long', + }, + 'auditd.log.items': { + category: 'auditd', + description: 'The number of items in an event. ', + name: 'auditd.log.items', + }, + 'auditd.log.item': { + category: 'auditd', + description: + 'The item field indicates which item out of the total number of items. This number is zero-based; a value of 0 means it is the first item. ', + name: 'auditd.log.item', + }, + 'auditd.log.tty': { + category: 'auditd', + name: 'auditd.log.tty', + type: 'keyword', + }, + 'auditd.log.a0': { + category: 'auditd', + description: 'The first argument to the system call. ', + name: 'auditd.log.a0', + }, + 'auditd.log.addr': { + category: 'auditd', + name: 'auditd.log.addr', + type: 'ip', + }, + 'auditd.log.rport': { + category: 'auditd', + name: 'auditd.log.rport', + type: 'long', + }, + 'auditd.log.laddr': { + category: 'auditd', + name: 'auditd.log.laddr', + type: 'ip', + }, + 'auditd.log.lport': { + category: 'auditd', + name: 'auditd.log.lport', + type: 'long', + }, + 'auditd.log.acct': { + category: 'auditd', + name: 'auditd.log.acct', + type: 'alias', + }, + 'auditd.log.pid': { + category: 'auditd', + name: 'auditd.log.pid', + type: 'alias', + }, + 'auditd.log.ppid': { + category: 'auditd', + name: 'auditd.log.ppid', + type: 'alias', + }, + 'auditd.log.res': { + category: 'auditd', + name: 'auditd.log.res', + type: 'alias', + }, + 'auditd.log.record_type': { + category: 'auditd', + name: 'auditd.log.record_type', + type: 'alias', + }, + 'auditd.log.geoip.continent_name': { + category: 'auditd', + name: 'auditd.log.geoip.continent_name', + type: 'alias', + }, + 'auditd.log.geoip.country_iso_code': { + category: 'auditd', + name: 'auditd.log.geoip.country_iso_code', + type: 'alias', + }, + 'auditd.log.geoip.location': { + category: 'auditd', + name: 'auditd.log.geoip.location', + type: 'alias', + }, + 'auditd.log.geoip.region_name': { + category: 'auditd', + name: 'auditd.log.geoip.region_name', + type: 'alias', + }, + 'auditd.log.geoip.city_name': { + category: 'auditd', + name: 'auditd.log.geoip.city_name', + type: 'alias', + }, + 'auditd.log.geoip.region_iso_code': { + category: 'auditd', + name: 'auditd.log.geoip.region_iso_code', + type: 'alias', + }, + 'auditd.log.arch': { + category: 'auditd', + name: 'auditd.log.arch', + type: 'alias', + }, + 'auditd.log.gid': { + category: 'auditd', + name: 'auditd.log.gid', + type: 'alias', + }, + 'auditd.log.uid': { + category: 'auditd', + name: 'auditd.log.uid', + type: 'alias', + }, + 'auditd.log.agid': { + category: 'auditd', + name: 'auditd.log.agid', + type: 'alias', + }, + 'auditd.log.auid': { + category: 'auditd', + name: 'auditd.log.auid', + type: 'alias', + }, + 'auditd.log.fsgid': { + category: 'auditd', + name: 'auditd.log.fsgid', + type: 'alias', + }, + 'auditd.log.fsuid': { + category: 'auditd', + name: 'auditd.log.fsuid', + type: 'alias', + }, + 'auditd.log.egid': { + category: 'auditd', + name: 'auditd.log.egid', + type: 'alias', + }, + 'auditd.log.euid': { + category: 'auditd', + name: 'auditd.log.euid', + type: 'alias', + }, + 'auditd.log.sgid': { + category: 'auditd', + name: 'auditd.log.sgid', + type: 'alias', + }, + 'auditd.log.suid': { + category: 'auditd', + name: 'auditd.log.suid', + type: 'alias', + }, + 'auditd.log.ogid': { + category: 'auditd', + name: 'auditd.log.ogid', + type: 'alias', + }, + 'auditd.log.ouid': { + category: 'auditd', + name: 'auditd.log.ouid', + type: 'alias', + }, + 'auditd.log.comm': { + category: 'auditd', + name: 'auditd.log.comm', + type: 'alias', + }, + 'auditd.log.exe': { + category: 'auditd', + name: 'auditd.log.exe', + type: 'alias', + }, + 'auditd.log.terminal': { + category: 'auditd', + name: 'auditd.log.terminal', + type: 'alias', + }, + 'auditd.log.msg': { + category: 'auditd', + name: 'auditd.log.msg', + type: 'alias', + }, + 'auditd.log.src': { + category: 'auditd', + name: 'auditd.log.src', + type: 'alias', + }, + 'auditd.log.dst': { + category: 'auditd', + name: 'auditd.log.dst', + type: 'alias', + }, + 'elasticsearch.component': { + category: 'elasticsearch', + description: 'Elasticsearch component from where the log event originated', + example: 'o.e.c.m.MetaDataCreateIndexService', + name: 'elasticsearch.component', + type: 'keyword', + }, + 'elasticsearch.cluster.uuid': { + category: 'elasticsearch', + description: 'UUID of the cluster', + example: 'GmvrbHlNTiSVYiPf8kxg9g', + name: 'elasticsearch.cluster.uuid', + type: 'keyword', + }, + 'elasticsearch.cluster.name': { + category: 'elasticsearch', + description: 'Name of the cluster', + example: 'docker-cluster', + name: 'elasticsearch.cluster.name', + type: 'keyword', + }, + 'elasticsearch.node.id': { + category: 'elasticsearch', + description: 'ID of the node', + example: 'DSiWcTyeThWtUXLB9J0BMw', + name: 'elasticsearch.node.id', + type: 'keyword', + }, + 'elasticsearch.node.name': { + category: 'elasticsearch', + description: 'Name of the node', + example: 'vWNJsZ3', + name: 'elasticsearch.node.name', + type: 'keyword', + }, + 'elasticsearch.index.name': { + category: 'elasticsearch', + description: 'Index name', + example: 'filebeat-test-input', + name: 'elasticsearch.index.name', + type: 'keyword', + }, + 'elasticsearch.index.id': { + category: 'elasticsearch', + description: 'Index id', + example: 'aOGgDwbURfCV57AScqbCgw', + name: 'elasticsearch.index.id', + type: 'keyword', + }, + 'elasticsearch.shard.id': { + category: 'elasticsearch', + description: 'Id of the shard', + example: '0', + name: 'elasticsearch.shard.id', + type: 'keyword', + }, + 'elasticsearch.audit.layer': { + category: 'elasticsearch', + description: 'The layer from which this event originated: rest, transport or ip_filter', + example: 'rest', + name: 'elasticsearch.audit.layer', + type: 'keyword', + }, + 'elasticsearch.audit.event_type': { + category: 'elasticsearch', + description: + 'The type of event that occurred: anonymous_access_denied, authentication_failed, access_denied, access_granted, connection_granted, connection_denied, tampered_request, run_as_granted, run_as_denied', + example: 'access_granted', + name: 'elasticsearch.audit.event_type', + type: 'keyword', + }, + 'elasticsearch.audit.origin.type': { + category: 'elasticsearch', + description: + 'Where the request originated: rest (request originated from a REST API request), transport (request was received on the transport channel), local_node (the local node issued the request)', + example: 'local_node', + name: 'elasticsearch.audit.origin.type', + type: 'keyword', + }, + 'elasticsearch.audit.realm': { + category: 'elasticsearch', + description: 'The authentication realm the authentication was validated against', + name: 'elasticsearch.audit.realm', + type: 'keyword', + }, + 'elasticsearch.audit.user.realm': { + category: 'elasticsearch', + description: "The user's authentication realm, if authenticated", + name: 'elasticsearch.audit.user.realm', + type: 'keyword', + }, + 'elasticsearch.audit.user.roles': { + category: 'elasticsearch', + description: 'Roles to which the principal belongs', + example: '["kibana_admin","beats_admin"]', + name: 'elasticsearch.audit.user.roles', + type: 'keyword', + }, + 'elasticsearch.audit.action': { + category: 'elasticsearch', + description: 'The name of the action that was executed', + example: 'cluster:monitor/main', + name: 'elasticsearch.audit.action', + type: 'keyword', + }, + 'elasticsearch.audit.url.params': { + category: 'elasticsearch', + description: 'REST URI parameters', + example: '{username=jacknich2}', + name: 'elasticsearch.audit.url.params', + }, + 'elasticsearch.audit.indices': { + category: 'elasticsearch', + description: 'Indices accessed by action', + example: '["foo-2019.01.04","foo-2019.01.03","foo-2019.01.06"]', + name: 'elasticsearch.audit.indices', + type: 'keyword', + }, + 'elasticsearch.audit.request.id': { + category: 'elasticsearch', + description: 'Unique ID of request', + example: 'WzL_kb6VSvOhAq0twPvHOQ', + name: 'elasticsearch.audit.request.id', + type: 'keyword', + }, + 'elasticsearch.audit.request.name': { + category: 'elasticsearch', + description: 'The type of request that was executed', + example: 'ClearScrollRequest', + name: 'elasticsearch.audit.request.name', + type: 'keyword', + }, + 'elasticsearch.audit.request_body': { + category: 'elasticsearch', + name: 'elasticsearch.audit.request_body', + type: 'alias', + }, + 'elasticsearch.audit.origin_address': { + category: 'elasticsearch', + name: 'elasticsearch.audit.origin_address', + type: 'alias', + }, + 'elasticsearch.audit.uri': { + category: 'elasticsearch', + name: 'elasticsearch.audit.uri', + type: 'alias', + }, + 'elasticsearch.audit.principal': { + category: 'elasticsearch', + name: 'elasticsearch.audit.principal', + type: 'alias', + }, + 'elasticsearch.audit.message': { + category: 'elasticsearch', + name: 'elasticsearch.audit.message', + type: 'text', + }, + 'elasticsearch.deprecation': { + category: 'elasticsearch', + description: '', + name: 'elasticsearch.deprecation', + type: 'group', + }, + 'elasticsearch.gc.phase.name': { + category: 'elasticsearch', + description: 'Name of the GC collection phase. ', + name: 'elasticsearch.gc.phase.name', + type: 'keyword', + }, + 'elasticsearch.gc.phase.duration_sec': { + category: 'elasticsearch', + description: 'Collection phase duration according to the Java virtual machine. ', + name: 'elasticsearch.gc.phase.duration_sec', + type: 'float', + }, + 'elasticsearch.gc.phase.scrub_symbol_table_time_sec': { + category: 'elasticsearch', + description: 'Pause time in seconds cleaning up symbol tables. ', + name: 'elasticsearch.gc.phase.scrub_symbol_table_time_sec', + type: 'float', + }, + 'elasticsearch.gc.phase.scrub_string_table_time_sec': { + category: 'elasticsearch', + description: 'Pause time in seconds cleaning up string tables. ', + name: 'elasticsearch.gc.phase.scrub_string_table_time_sec', + type: 'float', + }, + 'elasticsearch.gc.phase.weak_refs_processing_time_sec': { + category: 'elasticsearch', + description: 'Time spent processing weak references in seconds. ', + name: 'elasticsearch.gc.phase.weak_refs_processing_time_sec', + type: 'float', + }, + 'elasticsearch.gc.phase.parallel_rescan_time_sec': { + category: 'elasticsearch', + description: 'Time spent in seconds marking live objects while application is stopped. ', + name: 'elasticsearch.gc.phase.parallel_rescan_time_sec', + type: 'float', + }, + 'elasticsearch.gc.phase.class_unload_time_sec': { + category: 'elasticsearch', + description: 'Time spent unloading unused classes in seconds. ', + name: 'elasticsearch.gc.phase.class_unload_time_sec', + type: 'float', + }, + 'elasticsearch.gc.phase.cpu_time.user_sec': { + category: 'elasticsearch', + description: 'CPU time spent outside the kernel. ', + name: 'elasticsearch.gc.phase.cpu_time.user_sec', + type: 'float', + }, + 'elasticsearch.gc.phase.cpu_time.sys_sec': { + category: 'elasticsearch', + description: 'CPU time spent inside the kernel. ', + name: 'elasticsearch.gc.phase.cpu_time.sys_sec', + type: 'float', + }, + 'elasticsearch.gc.phase.cpu_time.real_sec': { + category: 'elasticsearch', + description: 'Total elapsed CPU time spent to complete the collection from start to finish. ', + name: 'elasticsearch.gc.phase.cpu_time.real_sec', + type: 'float', + }, + 'elasticsearch.gc.jvm_runtime_sec': { + category: 'elasticsearch', + description: 'The time from JVM start up in seconds, as a floating point number. ', + name: 'elasticsearch.gc.jvm_runtime_sec', + type: 'float', + }, + 'elasticsearch.gc.threads_total_stop_time_sec': { + category: 'elasticsearch', + description: 'Garbage collection threads total stop time seconds. ', + name: 'elasticsearch.gc.threads_total_stop_time_sec', + type: 'float', + }, + 'elasticsearch.gc.stopping_threads_time_sec': { + category: 'elasticsearch', + description: 'Time took to stop threads seconds. ', + name: 'elasticsearch.gc.stopping_threads_time_sec', + type: 'float', + }, + 'elasticsearch.gc.tags': { + category: 'elasticsearch', + description: 'GC logging tags. ', + name: 'elasticsearch.gc.tags', + type: 'keyword', + }, + 'elasticsearch.gc.heap.size_kb': { + category: 'elasticsearch', + description: 'Total heap size in kilobytes. ', + name: 'elasticsearch.gc.heap.size_kb', + type: 'integer', + }, + 'elasticsearch.gc.heap.used_kb': { + category: 'elasticsearch', + description: 'Used heap in kilobytes. ', + name: 'elasticsearch.gc.heap.used_kb', + type: 'integer', + }, + 'elasticsearch.gc.old_gen.size_kb': { + category: 'elasticsearch', + description: 'Total size of old generation in kilobytes. ', + name: 'elasticsearch.gc.old_gen.size_kb', + type: 'integer', + }, + 'elasticsearch.gc.old_gen.used_kb': { + category: 'elasticsearch', + description: 'Old generation occupancy in kilobytes. ', + name: 'elasticsearch.gc.old_gen.used_kb', + type: 'integer', + }, + 'elasticsearch.gc.young_gen.size_kb': { + category: 'elasticsearch', + description: 'Total size of young generation in kilobytes. ', + name: 'elasticsearch.gc.young_gen.size_kb', + type: 'integer', + }, + 'elasticsearch.gc.young_gen.used_kb': { + category: 'elasticsearch', + description: 'Young generation occupancy in kilobytes. ', + name: 'elasticsearch.gc.young_gen.used_kb', + type: 'integer', + }, + 'elasticsearch.server.stacktrace': { + category: 'elasticsearch', + name: 'elasticsearch.server.stacktrace', + }, + 'elasticsearch.server.gc.young.one': { + category: 'elasticsearch', + description: '', + example: '', + name: 'elasticsearch.server.gc.young.one', + type: 'long', + }, + 'elasticsearch.server.gc.young.two': { + category: 'elasticsearch', + description: '', + example: '', + name: 'elasticsearch.server.gc.young.two', + type: 'long', + }, + 'elasticsearch.server.gc.overhead_seq': { + category: 'elasticsearch', + description: 'Sequence number', + example: 3449992, + name: 'elasticsearch.server.gc.overhead_seq', + type: 'long', + }, + 'elasticsearch.server.gc.collection_duration.ms': { + category: 'elasticsearch', + description: 'Time spent in GC, in milliseconds', + example: 1600, + name: 'elasticsearch.server.gc.collection_duration.ms', + type: 'float', + }, + 'elasticsearch.server.gc.observation_duration.ms': { + category: 'elasticsearch', + description: 'Total time over which collection was observed, in milliseconds', + example: 1800, + name: 'elasticsearch.server.gc.observation_duration.ms', + type: 'float', + }, + 'elasticsearch.slowlog.logger': { + category: 'elasticsearch', + description: 'Logger name', + example: 'index.search.slowlog.fetch', + name: 'elasticsearch.slowlog.logger', + type: 'keyword', + }, + 'elasticsearch.slowlog.took': { + category: 'elasticsearch', + description: 'Time it took to execute the query', + example: '300ms', + name: 'elasticsearch.slowlog.took', + type: 'keyword', + }, + 'elasticsearch.slowlog.types': { + category: 'elasticsearch', + description: 'Types', + example: '', + name: 'elasticsearch.slowlog.types', + type: 'keyword', + }, + 'elasticsearch.slowlog.stats': { + category: 'elasticsearch', + description: 'Stats groups', + example: 'group1', + name: 'elasticsearch.slowlog.stats', + type: 'keyword', + }, + 'elasticsearch.slowlog.search_type': { + category: 'elasticsearch', + description: 'Search type', + example: 'QUERY_THEN_FETCH', + name: 'elasticsearch.slowlog.search_type', + type: 'keyword', + }, + 'elasticsearch.slowlog.source_query': { + category: 'elasticsearch', + description: 'Slow query', + example: '{"query":{"match_all":{"boost":1.0}}}', + name: 'elasticsearch.slowlog.source_query', + type: 'keyword', + }, + 'elasticsearch.slowlog.extra_source': { + category: 'elasticsearch', + description: 'Extra source information', + example: '', + name: 'elasticsearch.slowlog.extra_source', + type: 'keyword', + }, + 'elasticsearch.slowlog.total_hits': { + category: 'elasticsearch', + description: 'Total hits', + example: 42, + name: 'elasticsearch.slowlog.total_hits', + type: 'keyword', + }, + 'elasticsearch.slowlog.total_shards': { + category: 'elasticsearch', + description: 'Total queried shards', + example: 22, + name: 'elasticsearch.slowlog.total_shards', + type: 'keyword', + }, + 'elasticsearch.slowlog.routing': { + category: 'elasticsearch', + description: 'Routing', + example: 's01HZ2QBk9jw4gtgaFtn', + name: 'elasticsearch.slowlog.routing', + type: 'keyword', + }, + 'elasticsearch.slowlog.id': { + category: 'elasticsearch', + description: 'Id', + example: '', + name: 'elasticsearch.slowlog.id', + type: 'keyword', + }, + 'elasticsearch.slowlog.type': { + category: 'elasticsearch', + description: 'Type', + example: 'doc', + name: 'elasticsearch.slowlog.type', + type: 'keyword', + }, + 'elasticsearch.slowlog.source': { + category: 'elasticsearch', + description: 'Source of document that was indexed', + name: 'elasticsearch.slowlog.source', + type: 'keyword', + }, + 'haproxy.frontend_name': { + category: 'haproxy', + description: 'Name of the frontend (or listener) which received and processed the connection.', + name: 'haproxy.frontend_name', + }, + 'haproxy.backend_name': { + category: 'haproxy', + description: + 'Name of the backend (or listener) which was selected to manage the connection to the server.', + name: 'haproxy.backend_name', + }, + 'haproxy.server_name': { + category: 'haproxy', + description: 'Name of the last server to which the connection was sent.', + name: 'haproxy.server_name', + }, + 'haproxy.total_waiting_time_ms': { + category: 'haproxy', + description: 'Total time in milliseconds spent waiting in the various queues', + name: 'haproxy.total_waiting_time_ms', + type: 'long', + }, + 'haproxy.connection_wait_time_ms': { + category: 'haproxy', + description: + 'Total time in milliseconds spent waiting for the connection to establish to the final server', + name: 'haproxy.connection_wait_time_ms', + type: 'long', + }, + 'haproxy.bytes_read': { + category: 'haproxy', + description: 'Total number of bytes transmitted to the client when the log is emitted.', + name: 'haproxy.bytes_read', + type: 'long', + }, + 'haproxy.time_queue': { + category: 'haproxy', + description: 'Total time in milliseconds spent waiting in the various queues.', + name: 'haproxy.time_queue', + type: 'long', + }, + 'haproxy.time_backend_connect': { + category: 'haproxy', + description: + 'Total time in milliseconds spent waiting for the connection to establish to the final server, including retries.', + name: 'haproxy.time_backend_connect', + type: 'long', + }, + 'haproxy.server_queue': { + category: 'haproxy', + description: + 'Total number of requests which were processed before this one in the server queue.', + name: 'haproxy.server_queue', + type: 'long', + }, + 'haproxy.backend_queue': { + category: 'haproxy', + description: + "Total number of requests which were processed before this one in the backend's global queue.", + name: 'haproxy.backend_queue', + type: 'long', + }, + 'haproxy.bind_name': { + category: 'haproxy', + description: 'Name of the listening address which received the connection.', + name: 'haproxy.bind_name', + }, + 'haproxy.error_message': { + category: 'haproxy', + description: 'Error message logged by HAProxy in case of error.', + name: 'haproxy.error_message', + type: 'text', + }, + 'haproxy.source': { + category: 'haproxy', + description: 'The HAProxy source of the log', + name: 'haproxy.source', + type: 'keyword', + }, + 'haproxy.termination_state': { + category: 'haproxy', + description: 'Condition the session was in when the session ended.', + name: 'haproxy.termination_state', + }, + 'haproxy.mode': { + category: 'haproxy', + description: 'mode that the frontend is operating (TCP or HTTP)', + name: 'haproxy.mode', + type: 'keyword', + }, + 'haproxy.connections.active': { + category: 'haproxy', + description: + 'Total number of concurrent connections on the process when the session was logged.', + name: 'haproxy.connections.active', + type: 'long', + }, + 'haproxy.connections.frontend': { + category: 'haproxy', + description: + 'Total number of concurrent connections on the frontend when the session was logged.', + name: 'haproxy.connections.frontend', + type: 'long', + }, + 'haproxy.connections.backend': { + category: 'haproxy', + description: + 'Total number of concurrent connections handled by the backend when the session was logged.', + name: 'haproxy.connections.backend', + type: 'long', + }, + 'haproxy.connections.server': { + category: 'haproxy', + description: + 'Total number of concurrent connections still active on the server when the session was logged.', + name: 'haproxy.connections.server', + type: 'long', + }, + 'haproxy.connections.retries': { + category: 'haproxy', + description: + 'Number of connection retries experienced by this session when trying to connect to the server.', + name: 'haproxy.connections.retries', + type: 'long', + }, + 'haproxy.client.ip': { + category: 'haproxy', + name: 'haproxy.client.ip', + type: 'alias', + }, + 'haproxy.client.port': { + category: 'haproxy', + name: 'haproxy.client.port', + type: 'alias', + }, + 'haproxy.process_name': { + category: 'haproxy', + name: 'haproxy.process_name', + type: 'alias', + }, + 'haproxy.pid': { + category: 'haproxy', + name: 'haproxy.pid', + type: 'alias', + }, + 'haproxy.destination.port': { + category: 'haproxy', + name: 'haproxy.destination.port', + type: 'alias', + }, + 'haproxy.destination.ip': { + category: 'haproxy', + name: 'haproxy.destination.ip', + type: 'alias', + }, + 'haproxy.geoip.continent_name': { + category: 'haproxy', + name: 'haproxy.geoip.continent_name', + type: 'alias', + }, + 'haproxy.geoip.country_iso_code': { + category: 'haproxy', + name: 'haproxy.geoip.country_iso_code', + type: 'alias', + }, + 'haproxy.geoip.location': { + category: 'haproxy', + name: 'haproxy.geoip.location', + type: 'alias', + }, + 'haproxy.geoip.region_name': { + category: 'haproxy', + name: 'haproxy.geoip.region_name', + type: 'alias', + }, + 'haproxy.geoip.city_name': { + category: 'haproxy', + name: 'haproxy.geoip.city_name', + type: 'alias', + }, + 'haproxy.geoip.region_iso_code': { + category: 'haproxy', + name: 'haproxy.geoip.region_iso_code', + type: 'alias', + }, + 'haproxy.http.response.captured_cookie': { + category: 'haproxy', + description: + 'Optional "name=value" entry indicating that the client had this cookie in the response. ', + name: 'haproxy.http.response.captured_cookie', + }, + 'haproxy.http.response.captured_headers': { + category: 'haproxy', + description: + 'List of headers captured in the response due to the presence of the "capture response header" statement in the frontend. ', + name: 'haproxy.http.response.captured_headers', + type: 'keyword', + }, + 'haproxy.http.response.status_code': { + category: 'haproxy', + name: 'haproxy.http.response.status_code', + type: 'alias', + }, + 'haproxy.http.request.captured_cookie': { + category: 'haproxy', + description: + 'Optional "name=value" entry indicating that the server has returned a cookie with its request. ', + name: 'haproxy.http.request.captured_cookie', + }, + 'haproxy.http.request.captured_headers': { + category: 'haproxy', + description: + 'List of headers captured in the request due to the presence of the "capture request header" statement in the frontend. ', + name: 'haproxy.http.request.captured_headers', + type: 'keyword', + }, + 'haproxy.http.request.raw_request_line': { + category: 'haproxy', + description: + 'Complete HTTP request line, including the method, request and HTTP version string.', + name: 'haproxy.http.request.raw_request_line', + type: 'keyword', + }, + 'haproxy.http.request.time_wait_without_data_ms': { + category: 'haproxy', + description: + 'Total time in milliseconds spent waiting for the server to send a full HTTP response, not counting data.', + name: 'haproxy.http.request.time_wait_without_data_ms', + type: 'long', + }, + 'haproxy.http.request.time_wait_ms': { + category: 'haproxy', + description: + 'Total time in milliseconds spent waiting for a full HTTP request from the client (not counting body) after the first byte was received.', + name: 'haproxy.http.request.time_wait_ms', + type: 'long', + }, + 'haproxy.tcp.connection_waiting_time_ms': { + category: 'haproxy', + description: 'Total time in milliseconds elapsed between the accept and the last close', + name: 'haproxy.tcp.connection_waiting_time_ms', + type: 'long', + }, + 'icinga.debug.facility': { + category: 'icinga', + description: 'Specifies what component of Icinga logged the message. ', + name: 'icinga.debug.facility', + type: 'keyword', + }, + 'icinga.debug.severity': { + category: 'icinga', + name: 'icinga.debug.severity', + type: 'alias', + }, + 'icinga.debug.message': { + category: 'icinga', + name: 'icinga.debug.message', + type: 'alias', + }, + 'icinga.main.facility': { + category: 'icinga', + description: 'Specifies what component of Icinga logged the message. ', + name: 'icinga.main.facility', + type: 'keyword', + }, + 'icinga.main.severity': { + category: 'icinga', + name: 'icinga.main.severity', + type: 'alias', + }, + 'icinga.main.message': { + category: 'icinga', + name: 'icinga.main.message', + type: 'alias', + }, + 'icinga.startup.facility': { + category: 'icinga', + description: 'Specifies what component of Icinga logged the message. ', + name: 'icinga.startup.facility', + type: 'keyword', + }, + 'icinga.startup.severity': { + category: 'icinga', + name: 'icinga.startup.severity', + type: 'alias', + }, + 'icinga.startup.message': { + category: 'icinga', + name: 'icinga.startup.message', + type: 'alias', + }, + 'iis.access.sub_status': { + category: 'iis', + description: 'The HTTP substatus code. ', + name: 'iis.access.sub_status', + type: 'long', + }, + 'iis.access.win32_status': { + category: 'iis', + description: 'The Windows status code. ', + name: 'iis.access.win32_status', + type: 'long', + }, + 'iis.access.site_name': { + category: 'iis', + description: 'The site name and instance number. ', + name: 'iis.access.site_name', + type: 'keyword', + }, + 'iis.access.server_name': { + category: 'iis', + description: 'The name of the server on which the log file entry was generated. ', + name: 'iis.access.server_name', + type: 'keyword', + }, + 'iis.access.cookie': { + category: 'iis', + description: 'The content of the cookie sent or received, if any. ', + name: 'iis.access.cookie', + type: 'keyword', + }, + 'iis.access.body_received.bytes': { + category: 'iis', + name: 'iis.access.body_received.bytes', + type: 'alias', + }, + 'iis.access.body_sent.bytes': { + category: 'iis', + name: 'iis.access.body_sent.bytes', + type: 'alias', + }, + 'iis.access.server_ip': { + category: 'iis', + name: 'iis.access.server_ip', + type: 'alias', + }, + 'iis.access.method': { + category: 'iis', + name: 'iis.access.method', + type: 'alias', + }, + 'iis.access.url': { + category: 'iis', + name: 'iis.access.url', + type: 'alias', + }, + 'iis.access.query_string': { + category: 'iis', + name: 'iis.access.query_string', + type: 'alias', + }, + 'iis.access.port': { + category: 'iis', + name: 'iis.access.port', + type: 'alias', + }, + 'iis.access.user_name': { + category: 'iis', + name: 'iis.access.user_name', + type: 'alias', + }, + 'iis.access.remote_ip': { + category: 'iis', + name: 'iis.access.remote_ip', + type: 'alias', + }, + 'iis.access.referrer': { + category: 'iis', + name: 'iis.access.referrer', + type: 'alias', + }, + 'iis.access.response_code': { + category: 'iis', + name: 'iis.access.response_code', + type: 'alias', + }, + 'iis.access.http_version': { + category: 'iis', + name: 'iis.access.http_version', + type: 'alias', + }, + 'iis.access.hostname': { + category: 'iis', + name: 'iis.access.hostname', + type: 'alias', + }, + 'iis.access.user_agent.device': { + category: 'iis', + name: 'iis.access.user_agent.device', + type: 'alias', + }, + 'iis.access.user_agent.name': { + category: 'iis', + name: 'iis.access.user_agent.name', + type: 'alias', + }, + 'iis.access.user_agent.os': { + category: 'iis', + name: 'iis.access.user_agent.os', + type: 'alias', + }, + 'iis.access.user_agent.os_name': { + category: 'iis', + name: 'iis.access.user_agent.os_name', + type: 'alias', + }, + 'iis.access.user_agent.original': { + category: 'iis', + name: 'iis.access.user_agent.original', + type: 'alias', + }, + 'iis.access.geoip.continent_name': { + category: 'iis', + name: 'iis.access.geoip.continent_name', + type: 'alias', + }, + 'iis.access.geoip.country_iso_code': { + category: 'iis', + name: 'iis.access.geoip.country_iso_code', + type: 'alias', + }, + 'iis.access.geoip.location': { + category: 'iis', + name: 'iis.access.geoip.location', + type: 'alias', + }, + 'iis.access.geoip.region_name': { + category: 'iis', + name: 'iis.access.geoip.region_name', + type: 'alias', + }, + 'iis.access.geoip.city_name': { + category: 'iis', + name: 'iis.access.geoip.city_name', + type: 'alias', + }, + 'iis.access.geoip.region_iso_code': { + category: 'iis', + name: 'iis.access.geoip.region_iso_code', + type: 'alias', + }, + 'iis.error.reason_phrase': { + category: 'iis', + description: 'The HTTP reason phrase. ', + name: 'iis.error.reason_phrase', + type: 'keyword', + }, + 'iis.error.queue_name': { + category: 'iis', + description: 'The IIS application pool name. ', + name: 'iis.error.queue_name', + type: 'keyword', + }, + 'iis.error.remote_ip': { + category: 'iis', + name: 'iis.error.remote_ip', + type: 'alias', + }, + 'iis.error.remote_port': { + category: 'iis', + name: 'iis.error.remote_port', + type: 'alias', + }, + 'iis.error.server_ip': { + category: 'iis', + name: 'iis.error.server_ip', + type: 'alias', + }, + 'iis.error.server_port': { + category: 'iis', + name: 'iis.error.server_port', + type: 'alias', + }, + 'iis.error.http_version': { + category: 'iis', + name: 'iis.error.http_version', + type: 'alias', + }, + 'iis.error.method': { + category: 'iis', + name: 'iis.error.method', + type: 'alias', + }, + 'iis.error.url': { + category: 'iis', + name: 'iis.error.url', + type: 'alias', + }, + 'iis.error.response_code': { + category: 'iis', + name: 'iis.error.response_code', + type: 'alias', + }, + 'iis.error.geoip.continent_name': { + category: 'iis', + name: 'iis.error.geoip.continent_name', + type: 'alias', + }, + 'iis.error.geoip.country_iso_code': { + category: 'iis', + name: 'iis.error.geoip.country_iso_code', + type: 'alias', + }, + 'iis.error.geoip.location': { + category: 'iis', + name: 'iis.error.geoip.location', + type: 'alias', + }, + 'iis.error.geoip.region_name': { + category: 'iis', + name: 'iis.error.geoip.region_name', + type: 'alias', + }, + 'iis.error.geoip.city_name': { + category: 'iis', + name: 'iis.error.geoip.city_name', + type: 'alias', + }, + 'iis.error.geoip.region_iso_code': { + category: 'iis', + name: 'iis.error.geoip.region_iso_code', + type: 'alias', + }, + 'kafka.log.level': { + category: 'kafka', + name: 'kafka.log.level', + type: 'alias', + }, + 'kafka.log.message': { + category: 'kafka', + name: 'kafka.log.message', + type: 'alias', + }, + 'kafka.log.component': { + category: 'kafka', + description: 'Component the log is coming from. ', + name: 'kafka.log.component', + type: 'keyword', + }, + 'kafka.log.class': { + category: 'kafka', + description: 'Java class the log is coming from. ', + name: 'kafka.log.class', + type: 'keyword', + }, + 'kafka.log.thread': { + category: 'kafka', + description: 'Thread name the log is coming from. ', + name: 'kafka.log.thread', + type: 'keyword', + }, + 'kafka.log.trace.class': { + category: 'kafka', + description: 'Java class the trace is coming from. ', + name: 'kafka.log.trace.class', + type: 'keyword', + }, + 'kafka.log.trace.message': { + category: 'kafka', + description: 'Message part of the trace. ', + name: 'kafka.log.trace.message', + type: 'text', + }, + 'kibana.log.tags': { + category: 'kibana', + description: 'Kibana logging tags. ', + name: 'kibana.log.tags', + type: 'keyword', + }, + 'kibana.log.state': { + category: 'kibana', + description: 'Current state of Kibana. ', + name: 'kibana.log.state', + type: 'keyword', + }, + 'kibana.log.meta': { + category: 'kibana', + name: 'kibana.log.meta', + type: 'object', + }, + 'kibana.log.kibana.log.meta.req.headers.referer': { + category: 'kibana', + name: 'kibana.log.kibana.log.meta.req.headers.referer', + type: 'alias', + }, + 'kibana.log.kibana.log.meta.req.referer': { + category: 'kibana', + name: 'kibana.log.kibana.log.meta.req.referer', + type: 'alias', + }, + 'kibana.log.kibana.log.meta.req.headers.user-agent': { + category: 'kibana', + name: 'kibana.log.kibana.log.meta.req.headers.user-agent', + type: 'alias', + }, + 'kibana.log.kibana.log.meta.req.remoteAddress': { + category: 'kibana', + name: 'kibana.log.kibana.log.meta.req.remoteAddress', + type: 'alias', + }, + 'kibana.log.kibana.log.meta.req.url': { + category: 'kibana', + name: 'kibana.log.kibana.log.meta.req.url', + type: 'alias', + }, + 'kibana.log.kibana.log.meta.statusCode': { + category: 'kibana', + name: 'kibana.log.kibana.log.meta.statusCode', + type: 'alias', + }, + 'kibana.log.kibana.log.meta.method': { + category: 'kibana', + name: 'kibana.log.kibana.log.meta.method', + type: 'alias', + }, + 'logstash.log.module': { + category: 'logstash', + description: 'The module or class where the event originate. ', + name: 'logstash.log.module', + type: 'keyword', + }, + 'logstash.log.thread': { + category: 'logstash', + description: 'Information about the running thread where the log originate. ', + name: 'logstash.log.thread', + type: 'keyword', + }, + 'logstash.log.log_event': { + category: 'logstash', + description: 'key and value debugging information. ', + name: 'logstash.log.log_event', + type: 'object', + }, + 'logstash.log.pipeline_id': { + category: 'logstash', + description: 'The ID of the pipeline. ', + example: 'main', + name: 'logstash.log.pipeline_id', + type: 'keyword', + }, + 'logstash.log.message': { + category: 'logstash', + name: 'logstash.log.message', + type: 'alias', + }, + 'logstash.log.level': { + category: 'logstash', + name: 'logstash.log.level', + type: 'alias', + }, + 'logstash.slowlog.module': { + category: 'logstash', + description: 'The module or class where the event originate. ', + name: 'logstash.slowlog.module', + type: 'keyword', + }, + 'logstash.slowlog.thread': { + category: 'logstash', + description: 'Information about the running thread where the log originate. ', + name: 'logstash.slowlog.thread', + type: 'keyword', + }, + 'logstash.slowlog.event': { + category: 'logstash', + description: 'Raw dump of the original event ', + name: 'logstash.slowlog.event', + type: 'keyword', + }, + 'logstash.slowlog.plugin_name': { + category: 'logstash', + description: 'Name of the plugin ', + name: 'logstash.slowlog.plugin_name', + type: 'keyword', + }, + 'logstash.slowlog.plugin_type': { + category: 'logstash', + description: 'Type of the plugin: Inputs, Filters, Outputs or Codecs. ', + name: 'logstash.slowlog.plugin_type', + type: 'keyword', + }, + 'logstash.slowlog.took_in_millis': { + category: 'logstash', + description: 'Execution time for the plugin in milliseconds. ', + name: 'logstash.slowlog.took_in_millis', + type: 'long', + }, + 'logstash.slowlog.plugin_params': { + category: 'logstash', + description: 'String value of the plugin configuration ', + name: 'logstash.slowlog.plugin_params', + type: 'keyword', + }, + 'logstash.slowlog.plugin_params_object': { + category: 'logstash', + description: 'key -> value of the configuration used by the plugin. ', + name: 'logstash.slowlog.plugin_params_object', + type: 'object', + }, + 'logstash.slowlog.level': { + category: 'logstash', + name: 'logstash.slowlog.level', + type: 'alias', + }, + 'logstash.slowlog.took_in_nanos': { + category: 'logstash', + name: 'logstash.slowlog.took_in_nanos', + type: 'alias', + }, + 'mongodb.log.component': { + category: 'mongodb', + description: 'Functional categorization of message ', + example: 'COMMAND', + name: 'mongodb.log.component', + type: 'keyword', + }, + 'mongodb.log.context': { + category: 'mongodb', + description: 'Context of message ', + example: 'initandlisten', + name: 'mongodb.log.context', + type: 'keyword', + }, + 'mongodb.log.severity': { + category: 'mongodb', + name: 'mongodb.log.severity', + type: 'alias', + }, + 'mongodb.log.message': { + category: 'mongodb', + name: 'mongodb.log.message', + type: 'alias', + }, + 'mysql.thread_id': { + category: 'mysql', + description: 'The connection or thread ID for the query. ', + name: 'mysql.thread_id', + type: 'long', + }, + 'mysql.error.thread_id': { + category: 'mysql', + name: 'mysql.error.thread_id', + type: 'alias', + }, + 'mysql.error.level': { + category: 'mysql', + name: 'mysql.error.level', + type: 'alias', + }, + 'mysql.error.message': { + category: 'mysql', + name: 'mysql.error.message', + type: 'alias', + }, + 'mysql.slowlog.lock_time.sec': { + category: 'mysql', + description: + 'The amount of time the query waited for the lock to be available. The value is in seconds, as a floating point number. ', + name: 'mysql.slowlog.lock_time.sec', + type: 'float', + }, + 'mysql.slowlog.rows_sent': { + category: 'mysql', + description: 'The number of rows returned by the query. ', + name: 'mysql.slowlog.rows_sent', + type: 'long', + }, + 'mysql.slowlog.rows_examined': { + category: 'mysql', + description: 'The number of rows scanned by the query. ', + name: 'mysql.slowlog.rows_examined', + type: 'long', + }, + 'mysql.slowlog.rows_affected': { + category: 'mysql', + description: 'The number of rows modified by the query. ', + name: 'mysql.slowlog.rows_affected', + type: 'long', + }, + 'mysql.slowlog.bytes_sent': { + category: 'mysql', + description: 'The number of bytes sent to client. ', + name: 'mysql.slowlog.bytes_sent', + type: 'long', + format: 'bytes', + }, + 'mysql.slowlog.bytes_received': { + category: 'mysql', + description: 'The number of bytes received from client. ', + name: 'mysql.slowlog.bytes_received', + type: 'long', + format: 'bytes', + }, + 'mysql.slowlog.query': { + category: 'mysql', + description: 'The slow query. ', + name: 'mysql.slowlog.query', + }, + 'mysql.slowlog.id': { + category: 'mysql', + name: 'mysql.slowlog.id', + type: 'alias', + }, + 'mysql.slowlog.schema': { + category: 'mysql', + description: 'The schema where the slow query was executed. ', + name: 'mysql.slowlog.schema', + type: 'keyword', + }, + 'mysql.slowlog.current_user': { + category: 'mysql', + description: + 'Current authenticated user, used to determine access privileges. Can differ from the value for user. ', + name: 'mysql.slowlog.current_user', + type: 'keyword', + }, + 'mysql.slowlog.last_errno': { + category: 'mysql', + description: 'Last SQL error seen. ', + name: 'mysql.slowlog.last_errno', + type: 'keyword', + }, + 'mysql.slowlog.killed': { + category: 'mysql', + description: 'Code of the reason if the query was killed. ', + name: 'mysql.slowlog.killed', + type: 'keyword', + }, + 'mysql.slowlog.query_cache_hit': { + category: 'mysql', + description: 'Whether the query cache was hit. ', + name: 'mysql.slowlog.query_cache_hit', + type: 'boolean', + }, + 'mysql.slowlog.tmp_table': { + category: 'mysql', + description: 'Whether a temporary table was used to resolve the query. ', + name: 'mysql.slowlog.tmp_table', + type: 'boolean', + }, + 'mysql.slowlog.tmp_table_on_disk': { + category: 'mysql', + description: 'Whether the query needed temporary tables on disk. ', + name: 'mysql.slowlog.tmp_table_on_disk', + type: 'boolean', + }, + 'mysql.slowlog.tmp_tables': { + category: 'mysql', + description: 'Number of temporary tables created for this query ', + name: 'mysql.slowlog.tmp_tables', + type: 'long', + }, + 'mysql.slowlog.tmp_disk_tables': { + category: 'mysql', + description: 'Number of temporary tables created on disk for this query. ', + name: 'mysql.slowlog.tmp_disk_tables', + type: 'long', + }, + 'mysql.slowlog.tmp_table_sizes': { + category: 'mysql', + description: 'Size of temporary tables created for this query.', + name: 'mysql.slowlog.tmp_table_sizes', + type: 'long', + format: 'bytes', + }, + 'mysql.slowlog.filesort': { + category: 'mysql', + description: 'Whether filesort optimization was used. ', + name: 'mysql.slowlog.filesort', + type: 'boolean', + }, + 'mysql.slowlog.filesort_on_disk': { + category: 'mysql', + description: 'Whether filesort optimization was used and it needed temporary tables on disk. ', + name: 'mysql.slowlog.filesort_on_disk', + type: 'boolean', + }, + 'mysql.slowlog.priority_queue': { + category: 'mysql', + description: 'Whether a priority queue was used for filesort. ', + name: 'mysql.slowlog.priority_queue', + type: 'boolean', + }, + 'mysql.slowlog.full_scan': { + category: 'mysql', + description: 'Whether a full table scan was needed for the slow query. ', + name: 'mysql.slowlog.full_scan', + type: 'boolean', + }, + 'mysql.slowlog.full_join': { + category: 'mysql', + description: + 'Whether a full join was needed for the slow query (no indexes were used for joins). ', + name: 'mysql.slowlog.full_join', + type: 'boolean', + }, + 'mysql.slowlog.merge_passes': { + category: 'mysql', + description: 'Number of merge passes executed for the query. ', + name: 'mysql.slowlog.merge_passes', + type: 'long', + }, + 'mysql.slowlog.sort_merge_passes': { + category: 'mysql', + description: 'Number of merge passes that the sort algorithm has had to do. ', + name: 'mysql.slowlog.sort_merge_passes', + type: 'long', + }, + 'mysql.slowlog.sort_range_count': { + category: 'mysql', + description: 'Number of sorts that were done using ranges. ', + name: 'mysql.slowlog.sort_range_count', + type: 'long', + }, + 'mysql.slowlog.sort_rows': { + category: 'mysql', + description: 'Number of sorted rows. ', + name: 'mysql.slowlog.sort_rows', + type: 'long', + }, + 'mysql.slowlog.sort_scan_count': { + category: 'mysql', + description: 'Number of sorts that were done by scanning the table. ', + name: 'mysql.slowlog.sort_scan_count', + type: 'long', + }, + 'mysql.slowlog.log_slow_rate_type': { + category: 'mysql', + description: + 'Type of slow log rate limit, it can be `session` if the rate limit is applied per session, or `query` if it applies per query. ', + name: 'mysql.slowlog.log_slow_rate_type', + type: 'keyword', + }, + 'mysql.slowlog.log_slow_rate_limit': { + category: 'mysql', + description: + 'Slow log rate limit, a value of 100 means that one in a hundred queries or sessions are being logged. ', + name: 'mysql.slowlog.log_slow_rate_limit', + type: 'keyword', + }, + 'mysql.slowlog.read_first': { + category: 'mysql', + description: 'The number of times the first entry in an index was read. ', + name: 'mysql.slowlog.read_first', + type: 'long', + }, + 'mysql.slowlog.read_last': { + category: 'mysql', + description: 'The number of times the last key in an index was read. ', + name: 'mysql.slowlog.read_last', + type: 'long', + }, + 'mysql.slowlog.read_key': { + category: 'mysql', + description: 'The number of requests to read a row based on a key. ', + name: 'mysql.slowlog.read_key', + type: 'long', + }, + 'mysql.slowlog.read_next': { + category: 'mysql', + description: 'The number of requests to read the next row in key order. ', + name: 'mysql.slowlog.read_next', + type: 'long', + }, + 'mysql.slowlog.read_prev': { + category: 'mysql', + description: 'The number of requests to read the previous row in key order. ', + name: 'mysql.slowlog.read_prev', + type: 'long', + }, + 'mysql.slowlog.read_rnd': { + category: 'mysql', + description: 'The number of requests to read a row based on a fixed position. ', + name: 'mysql.slowlog.read_rnd', + type: 'long', + }, + 'mysql.slowlog.read_rnd_next': { + category: 'mysql', + description: 'The number of requests to read the next row in the data file. ', + name: 'mysql.slowlog.read_rnd_next', + type: 'long', + }, + 'mysql.slowlog.innodb.trx_id': { + category: 'mysql', + description: 'Transaction ID ', + name: 'mysql.slowlog.innodb.trx_id', + type: 'keyword', + }, + 'mysql.slowlog.innodb.io_r_ops': { + category: 'mysql', + description: 'Number of page read operations. ', + name: 'mysql.slowlog.innodb.io_r_ops', + type: 'long', + }, + 'mysql.slowlog.innodb.io_r_bytes': { + category: 'mysql', + description: 'Bytes read during page read operations. ', + name: 'mysql.slowlog.innodb.io_r_bytes', + type: 'long', + format: 'bytes', + }, + 'mysql.slowlog.innodb.io_r_wait.sec': { + category: 'mysql', + description: 'How long it took to read all needed data from storage. ', + name: 'mysql.slowlog.innodb.io_r_wait.sec', + type: 'long', + }, + 'mysql.slowlog.innodb.rec_lock_wait.sec': { + category: 'mysql', + description: 'How long the query waited for locks. ', + name: 'mysql.slowlog.innodb.rec_lock_wait.sec', + type: 'long', + }, + 'mysql.slowlog.innodb.queue_wait.sec': { + category: 'mysql', + description: + 'How long the query waited to enter the InnoDB queue and to be executed once in the queue. ', + name: 'mysql.slowlog.innodb.queue_wait.sec', + type: 'long', + }, + 'mysql.slowlog.innodb.pages_distinct': { + category: 'mysql', + description: 'Approximated count of pages accessed to execute the query. ', + name: 'mysql.slowlog.innodb.pages_distinct', + type: 'long', + }, + 'mysql.slowlog.user': { + category: 'mysql', + name: 'mysql.slowlog.user', + type: 'alias', + }, + 'mysql.slowlog.host': { + category: 'mysql', + name: 'mysql.slowlog.host', + type: 'alias', + }, + 'mysql.slowlog.ip': { + category: 'mysql', + name: 'mysql.slowlog.ip', + type: 'alias', + }, + 'nats.log.client.id': { + category: 'nats', + description: 'The id of the client ', + name: 'nats.log.client.id', + type: 'integer', + }, + 'nats.log.msg.bytes': { + category: 'nats', + description: 'Size of the payload in bytes ', + name: 'nats.log.msg.bytes', + type: 'long', + format: 'bytes', + }, + 'nats.log.msg.type': { + category: 'nats', + description: 'The protocol message type ', + name: 'nats.log.msg.type', + type: 'keyword', + }, + 'nats.log.msg.subject': { + category: 'nats', + description: 'Subject name this message was received on ', + name: 'nats.log.msg.subject', + type: 'keyword', + }, + 'nats.log.msg.sid': { + category: 'nats', + description: 'The unique alphanumeric subscription ID of the subject ', + name: 'nats.log.msg.sid', + type: 'integer', + }, + 'nats.log.msg.reply_to': { + category: 'nats', + description: 'The inbox subject on which the publisher is listening for responses ', + name: 'nats.log.msg.reply_to', + type: 'keyword', + }, + 'nats.log.msg.max_messages': { + category: 'nats', + description: 'An optional number of messages to wait for before automatically unsubscribing ', + name: 'nats.log.msg.max_messages', + type: 'integer', + }, + 'nats.log.msg.error.message': { + category: 'nats', + description: 'Details about the error occurred ', + name: 'nats.log.msg.error.message', + type: 'text', + }, + 'nats.log.msg.queue_group': { + category: 'nats', + description: 'The queue group which subscriber will join ', + name: 'nats.log.msg.queue_group', + type: 'text', + }, + 'nginx.access.remote_ip_list': { + category: 'nginx', + description: + 'An array of remote IP addresses. It is a list because it is common to include, besides the client IP address, IP addresses from headers like `X-Forwarded-For`. Real source IP is restored to `source.ip`. ', + name: 'nginx.access.remote_ip_list', + type: 'array', + }, + 'nginx.access.body_sent.bytes': { + category: 'nginx', + name: 'nginx.access.body_sent.bytes', + type: 'alias', + }, + 'nginx.access.user_name': { + category: 'nginx', + name: 'nginx.access.user_name', + type: 'alias', + }, + 'nginx.access.method': { + category: 'nginx', + name: 'nginx.access.method', + type: 'alias', + }, + 'nginx.access.url': { + category: 'nginx', + name: 'nginx.access.url', + type: 'alias', + }, + 'nginx.access.http_version': { + category: 'nginx', + name: 'nginx.access.http_version', + type: 'alias', + }, + 'nginx.access.response_code': { + category: 'nginx', + name: 'nginx.access.response_code', + type: 'alias', + }, + 'nginx.access.referrer': { + category: 'nginx', + name: 'nginx.access.referrer', + type: 'alias', + }, + 'nginx.access.agent': { + category: 'nginx', + name: 'nginx.access.agent', + type: 'alias', + }, + 'nginx.access.user_agent.device': { + category: 'nginx', + name: 'nginx.access.user_agent.device', + type: 'alias', + }, + 'nginx.access.user_agent.name': { + category: 'nginx', + name: 'nginx.access.user_agent.name', + type: 'alias', + }, + 'nginx.access.user_agent.os': { + category: 'nginx', + name: 'nginx.access.user_agent.os', + type: 'alias', + }, + 'nginx.access.user_agent.os_name': { + category: 'nginx', + name: 'nginx.access.user_agent.os_name', + type: 'alias', + }, + 'nginx.access.user_agent.original': { + category: 'nginx', + name: 'nginx.access.user_agent.original', + type: 'alias', + }, + 'nginx.access.geoip.continent_name': { + category: 'nginx', + name: 'nginx.access.geoip.continent_name', + type: 'alias', + }, + 'nginx.access.geoip.country_iso_code': { + category: 'nginx', + name: 'nginx.access.geoip.country_iso_code', + type: 'alias', + }, + 'nginx.access.geoip.location': { + category: 'nginx', + name: 'nginx.access.geoip.location', + type: 'alias', + }, + 'nginx.access.geoip.region_name': { + category: 'nginx', + name: 'nginx.access.geoip.region_name', + type: 'alias', + }, + 'nginx.access.geoip.city_name': { + category: 'nginx', + name: 'nginx.access.geoip.city_name', + type: 'alias', + }, + 'nginx.access.geoip.region_iso_code': { + category: 'nginx', + name: 'nginx.access.geoip.region_iso_code', + type: 'alias', + }, + 'nginx.error.connection_id': { + category: 'nginx', + description: 'Connection identifier. ', + name: 'nginx.error.connection_id', + type: 'long', + }, + 'nginx.error.level': { + category: 'nginx', + name: 'nginx.error.level', + type: 'alias', + }, + 'nginx.error.pid': { + category: 'nginx', + name: 'nginx.error.pid', + type: 'alias', + }, + 'nginx.error.tid': { + category: 'nginx', + name: 'nginx.error.tid', + type: 'alias', + }, + 'nginx.error.message': { + category: 'nginx', + name: 'nginx.error.message', + type: 'alias', + }, + 'nginx.ingress_controller.remote_ip_list': { + category: 'nginx', + description: + 'An array of remote IP addresses. It is a list because it is common to include, besides the client IP address, IP addresses from headers like `X-Forwarded-For`. Real source IP is restored to `source.ip`. ', + name: 'nginx.ingress_controller.remote_ip_list', + type: 'array', + }, + 'nginx.ingress_controller.http.request.length': { + category: 'nginx', + description: 'The request length (including request line, header, and request body) ', + name: 'nginx.ingress_controller.http.request.length', + type: 'long', + format: 'bytes', + }, + 'nginx.ingress_controller.http.request.time': { + category: 'nginx', + description: 'Time elapsed since the first bytes were read from the client ', + name: 'nginx.ingress_controller.http.request.time', + type: 'double', + format: 'duration', + }, + 'nginx.ingress_controller.upstream.name': { + category: 'nginx', + description: 'The name of the upstream. ', + name: 'nginx.ingress_controller.upstream.name', + type: 'keyword', + }, + 'nginx.ingress_controller.upstream.alternative_name': { + category: 'nginx', + description: 'The name of the alternative upstream. ', + name: 'nginx.ingress_controller.upstream.alternative_name', + type: 'keyword', + }, + 'nginx.ingress_controller.upstream.response.length': { + category: 'nginx', + description: 'The length of the response obtained from the upstream server ', + name: 'nginx.ingress_controller.upstream.response.length', + type: 'long', + format: 'bytes', + }, + 'nginx.ingress_controller.upstream.response.time': { + category: 'nginx', + description: + 'The time spent on receiving the response from the upstream server as seconds with millisecond resolution ', + name: 'nginx.ingress_controller.upstream.response.time', + type: 'double', + format: 'duration', + }, + 'nginx.ingress_controller.upstream.response.status_code': { + category: 'nginx', + description: 'The status code of the response obtained from the upstream server ', + name: 'nginx.ingress_controller.upstream.response.status_code', + type: 'long', + }, + 'nginx.ingress_controller.http.request.id': { + category: 'nginx', + description: 'The randomly generated ID of the request ', + name: 'nginx.ingress_controller.http.request.id', + type: 'keyword', + }, + 'nginx.ingress_controller.upstream.ip': { + category: 'nginx', + description: + 'The IP address of the upstream server. If several servers were contacted during request processing, their addresses are separated by commas. ', + name: 'nginx.ingress_controller.upstream.ip', + type: 'ip', + }, + 'nginx.ingress_controller.upstream.port': { + category: 'nginx', + description: 'The port of the upstream server. ', + name: 'nginx.ingress_controller.upstream.port', + type: 'long', + }, + 'nginx.ingress_controller.body_sent.bytes': { + category: 'nginx', + name: 'nginx.ingress_controller.body_sent.bytes', + type: 'alias', + }, + 'nginx.ingress_controller.user_name': { + category: 'nginx', + name: 'nginx.ingress_controller.user_name', + type: 'alias', + }, + 'nginx.ingress_controller.method': { + category: 'nginx', + name: 'nginx.ingress_controller.method', + type: 'alias', + }, + 'nginx.ingress_controller.url': { + category: 'nginx', + name: 'nginx.ingress_controller.url', + type: 'alias', + }, + 'nginx.ingress_controller.http_version': { + category: 'nginx', + name: 'nginx.ingress_controller.http_version', + type: 'alias', + }, + 'nginx.ingress_controller.response_code': { + category: 'nginx', + name: 'nginx.ingress_controller.response_code', + type: 'alias', + }, + 'nginx.ingress_controller.referrer': { + category: 'nginx', + name: 'nginx.ingress_controller.referrer', + type: 'alias', + }, + 'nginx.ingress_controller.agent': { + category: 'nginx', + name: 'nginx.ingress_controller.agent', + type: 'alias', + }, + 'nginx.ingress_controller.user_agent.device': { + category: 'nginx', + name: 'nginx.ingress_controller.user_agent.device', + type: 'alias', + }, + 'nginx.ingress_controller.user_agent.name': { + category: 'nginx', + name: 'nginx.ingress_controller.user_agent.name', + type: 'alias', + }, + 'nginx.ingress_controller.user_agent.os': { + category: 'nginx', + name: 'nginx.ingress_controller.user_agent.os', + type: 'alias', + }, + 'nginx.ingress_controller.user_agent.os_name': { + category: 'nginx', + name: 'nginx.ingress_controller.user_agent.os_name', + type: 'alias', + }, + 'nginx.ingress_controller.user_agent.original': { + category: 'nginx', + name: 'nginx.ingress_controller.user_agent.original', + type: 'alias', + }, + 'nginx.ingress_controller.geoip.continent_name': { + category: 'nginx', + name: 'nginx.ingress_controller.geoip.continent_name', + type: 'alias', + }, + 'nginx.ingress_controller.geoip.country_iso_code': { + category: 'nginx', + name: 'nginx.ingress_controller.geoip.country_iso_code', + type: 'alias', + }, + 'nginx.ingress_controller.geoip.location': { + category: 'nginx', + name: 'nginx.ingress_controller.geoip.location', + type: 'alias', + }, + 'nginx.ingress_controller.geoip.region_name': { + category: 'nginx', + name: 'nginx.ingress_controller.geoip.region_name', + type: 'alias', + }, + 'nginx.ingress_controller.geoip.city_name': { + category: 'nginx', + name: 'nginx.ingress_controller.geoip.city_name', + type: 'alias', + }, + 'nginx.ingress_controller.geoip.region_iso_code': { + category: 'nginx', + name: 'nginx.ingress_controller.geoip.region_iso_code', + type: 'alias', + }, + 'osquery.result.name': { + category: 'osquery', + description: 'The name of the query that generated this event. ', + name: 'osquery.result.name', + type: 'keyword', + }, + 'osquery.result.action': { + category: 'osquery', + description: + 'For incremental data, marks whether the entry was added or removed. It can be one of "added", "removed", or "snapshot". ', + name: 'osquery.result.action', + type: 'keyword', + }, + 'osquery.result.host_identifier': { + category: 'osquery', + description: + 'The identifier for the host on which the osquery agent is running. Normally the hostname. ', + name: 'osquery.result.host_identifier', + type: 'keyword', + }, + 'osquery.result.unix_time': { + category: 'osquery', + description: + 'Unix timestamp of the event, in seconds since the epoch. Used for computing the `@timestamp` column. ', + name: 'osquery.result.unix_time', + type: 'long', + }, + 'osquery.result.calendar_time': { + category: 'osquery', + description: 'String representation of the collection time, as formatted by osquery. ', + name: 'osquery.result.calendar_time', + type: 'keyword', + }, + 'postgresql.log.timestamp': { + category: 'postgresql', + description: 'The timestamp from the log line. ', + name: 'postgresql.log.timestamp', + }, + 'postgresql.log.core_id': { + category: 'postgresql', + description: 'Core id ', + name: 'postgresql.log.core_id', + type: 'long', + }, + 'postgresql.log.database': { + category: 'postgresql', + description: 'Name of database ', + example: 'mydb', + name: 'postgresql.log.database', + }, + 'postgresql.log.query': { + category: 'postgresql', + description: 'Query statement. ', + example: 'SELECT * FROM users;', + name: 'postgresql.log.query', + }, + 'postgresql.log.query_step': { + category: 'postgresql', + description: + 'Statement step when using extended query protocol (one of statement, parse, bind or execute) ', + example: 'parse', + name: 'postgresql.log.query_step', + }, + 'postgresql.log.query_name': { + category: 'postgresql', + description: + 'Name given to a query when using extended query protocol. If it is "", or not present, this field is ignored. ', + example: 'pdo_stmt_00000001', + name: 'postgresql.log.query_name', + }, + 'postgresql.log.error.code': { + category: 'postgresql', + description: 'Error code returned by Postgres (if any)', + name: 'postgresql.log.error.code', + type: 'long', + }, + 'postgresql.log.timezone': { + category: 'postgresql', + name: 'postgresql.log.timezone', + type: 'alias', + }, + 'postgresql.log.thread_id': { + category: 'postgresql', + name: 'postgresql.log.thread_id', + type: 'alias', + }, + 'postgresql.log.user': { + category: 'postgresql', + name: 'postgresql.log.user', + type: 'alias', + }, + 'postgresql.log.level': { + category: 'postgresql', + name: 'postgresql.log.level', + type: 'alias', + }, + 'postgresql.log.message': { + category: 'postgresql', + name: 'postgresql.log.message', + type: 'alias', + }, + 'redis.log.role': { + category: 'redis', + description: + 'The role of the Redis instance. Can be one of `master`, `slave`, `child` (for RDF/AOF writing child), or `sentinel`. ', + name: 'redis.log.role', + type: 'keyword', + }, + 'redis.log.pid': { + category: 'redis', + name: 'redis.log.pid', + type: 'alias', + }, + 'redis.log.level': { + category: 'redis', + name: 'redis.log.level', + type: 'alias', + }, + 'redis.log.message': { + category: 'redis', + name: 'redis.log.message', + type: 'alias', + }, + 'redis.slowlog.cmd': { + category: 'redis', + description: 'The command executed. ', + name: 'redis.slowlog.cmd', + type: 'keyword', + }, + 'redis.slowlog.duration.us': { + category: 'redis', + description: 'How long it took to execute the command in microseconds. ', + name: 'redis.slowlog.duration.us', + type: 'long', + }, + 'redis.slowlog.id': { + category: 'redis', + description: 'The ID of the query. ', + name: 'redis.slowlog.id', + type: 'long', + }, + 'redis.slowlog.key': { + category: 'redis', + description: 'The key on which the command was executed. ', + name: 'redis.slowlog.key', + type: 'keyword', + }, + 'redis.slowlog.args': { + category: 'redis', + description: 'The arguments with which the command was called. ', + name: 'redis.slowlog.args', + type: 'keyword', + }, + 'santa.action': { + category: 'santa', + description: 'Action', + example: 'EXEC', + name: 'santa.action', + type: 'keyword', + }, + 'santa.decision': { + category: 'santa', + description: 'Decision that santad took.', + example: 'ALLOW', + name: 'santa.decision', + type: 'keyword', + }, + 'santa.reason': { + category: 'santa', + description: 'Reason for the decsision.', + example: 'CERT', + name: 'santa.reason', + type: 'keyword', + }, + 'santa.mode': { + category: 'santa', + description: 'Operating mode of Santa.', + example: 'M', + name: 'santa.mode', + type: 'keyword', + }, + 'santa.disk.volume': { + category: 'santa', + description: 'The volume name.', + name: 'santa.disk.volume', + }, + 'santa.disk.bus': { + category: 'santa', + description: 'The disk bus protocol.', + name: 'santa.disk.bus', + }, + 'santa.disk.serial': { + category: 'santa', + description: 'The disk serial number.', + name: 'santa.disk.serial', + }, + 'santa.disk.bsdname': { + category: 'santa', + description: 'The disk BSD name.', + example: 'disk1s3', + name: 'santa.disk.bsdname', + }, + 'santa.disk.model': { + category: 'santa', + description: 'The disk model.', + example: 'APPLE SSD SM0512L', + name: 'santa.disk.model', + }, + 'santa.disk.fs': { + category: 'santa', + description: 'The disk volume kind (filesystem type).', + example: 'apfs', + name: 'santa.disk.fs', + }, + 'santa.disk.mount': { + category: 'santa', + description: 'The disk volume path.', + name: 'santa.disk.mount', + }, + 'santa.certificate.common_name': { + category: 'santa', + description: 'Common name from code signing certificate.', + name: 'santa.certificate.common_name', + type: 'keyword', + }, + 'santa.certificate.sha256': { + category: 'santa', + description: 'SHA256 hash of code signing certificate.', + name: 'santa.certificate.sha256', + type: 'keyword', + }, + 'system.auth.timestamp': { + category: 'system', + name: 'system.auth.timestamp', + type: 'alias', + }, + 'system.auth.hostname': { + category: 'system', + name: 'system.auth.hostname', + type: 'alias', + }, + 'system.auth.program': { + category: 'system', + name: 'system.auth.program', + type: 'alias', + }, + 'system.auth.pid': { + category: 'system', + name: 'system.auth.pid', + type: 'alias', + }, + 'system.auth.message': { + category: 'system', + name: 'system.auth.message', + type: 'alias', + }, + 'system.auth.user': { + category: 'system', + name: 'system.auth.user', + type: 'alias', + }, + 'system.auth.ssh.method': { + category: 'system', + description: 'The SSH authentication method. Can be one of "password" or "publickey". ', + name: 'system.auth.ssh.method', + }, + 'system.auth.ssh.signature': { + category: 'system', + description: 'The signature of the client public key. ', + name: 'system.auth.ssh.signature', + }, + 'system.auth.ssh.dropped_ip': { + category: 'system', + description: 'The client IP from SSH connections that are open and immediately dropped. ', + name: 'system.auth.ssh.dropped_ip', + type: 'ip', + }, + 'system.auth.ssh.event': { + category: 'system', + description: 'The SSH event as found in the logs (Accepted, Invalid, Failed, etc.) ', + example: 'Accepted', + name: 'system.auth.ssh.event', + }, + 'system.auth.ssh.ip': { + category: 'system', + name: 'system.auth.ssh.ip', + type: 'alias', + }, + 'system.auth.ssh.port': { + category: 'system', + name: 'system.auth.ssh.port', + type: 'alias', + }, + 'system.auth.ssh.geoip.continent_name': { + category: 'system', + name: 'system.auth.ssh.geoip.continent_name', + type: 'alias', + }, + 'system.auth.ssh.geoip.country_iso_code': { + category: 'system', + name: 'system.auth.ssh.geoip.country_iso_code', + type: 'alias', + }, + 'system.auth.ssh.geoip.location': { + category: 'system', + name: 'system.auth.ssh.geoip.location', + type: 'alias', + }, + 'system.auth.ssh.geoip.region_name': { + category: 'system', + name: 'system.auth.ssh.geoip.region_name', + type: 'alias', + }, + 'system.auth.ssh.geoip.city_name': { + category: 'system', + name: 'system.auth.ssh.geoip.city_name', + type: 'alias', + }, + 'system.auth.ssh.geoip.region_iso_code': { + category: 'system', + name: 'system.auth.ssh.geoip.region_iso_code', + type: 'alias', + }, + 'system.auth.sudo.error': { + category: 'system', + description: 'The error message in case the sudo command failed. ', + example: 'user NOT in sudoers', + name: 'system.auth.sudo.error', + }, + 'system.auth.sudo.tty': { + category: 'system', + description: 'The TTY where the sudo command is executed. ', + name: 'system.auth.sudo.tty', + }, + 'system.auth.sudo.pwd': { + category: 'system', + description: 'The current directory where the sudo command is executed. ', + name: 'system.auth.sudo.pwd', + }, + 'system.auth.sudo.user': { + category: 'system', + description: 'The target user to which the sudo command is switching. ', + example: 'root', + name: 'system.auth.sudo.user', + }, + 'system.auth.sudo.command': { + category: 'system', + description: 'The command executed via sudo. ', + name: 'system.auth.sudo.command', + }, + 'system.auth.useradd.home': { + category: 'system', + description: 'The home folder for the new user.', + name: 'system.auth.useradd.home', + }, + 'system.auth.useradd.shell': { + category: 'system', + description: 'The default shell for the new user.', + name: 'system.auth.useradd.shell', + }, + 'system.auth.useradd.name': { + category: 'system', + name: 'system.auth.useradd.name', + type: 'alias', + }, + 'system.auth.useradd.uid': { + category: 'system', + name: 'system.auth.useradd.uid', + type: 'alias', + }, + 'system.auth.useradd.gid': { + category: 'system', + name: 'system.auth.useradd.gid', + type: 'alias', + }, + 'system.auth.groupadd.name': { + category: 'system', + name: 'system.auth.groupadd.name', + type: 'alias', + }, + 'system.auth.groupadd.gid': { + category: 'system', + name: 'system.auth.groupadd.gid', + type: 'alias', + }, + 'system.syslog.timestamp': { + category: 'system', + name: 'system.syslog.timestamp', + type: 'alias', + }, + 'system.syslog.hostname': { + category: 'system', + name: 'system.syslog.hostname', + type: 'alias', + }, + 'system.syslog.program': { + category: 'system', + name: 'system.syslog.program', + type: 'alias', + }, + 'system.syslog.pid': { + category: 'system', + name: 'system.syslog.pid', + type: 'alias', + }, + 'system.syslog.message': { + category: 'system', + name: 'system.syslog.message', + type: 'alias', + }, + 'traefik.access.user_identifier': { + category: 'traefik', + description: 'Is the RFC 1413 identity of the client ', + name: 'traefik.access.user_identifier', + type: 'keyword', + }, + 'traefik.access.request_count': { + category: 'traefik', + description: 'The number of requests ', + name: 'traefik.access.request_count', + type: 'long', + }, + 'traefik.access.frontend_name': { + category: 'traefik', + description: 'The name of the frontend used ', + name: 'traefik.access.frontend_name', + type: 'keyword', + }, + 'traefik.access.backend_url': { + category: 'traefik', + description: 'The url of the backend where request is forwarded', + name: 'traefik.access.backend_url', + type: 'keyword', + }, + 'traefik.access.body_sent.bytes': { + category: 'traefik', + name: 'traefik.access.body_sent.bytes', + type: 'alias', + }, + 'traefik.access.remote_ip': { + category: 'traefik', + name: 'traefik.access.remote_ip', + type: 'alias', + }, + 'traefik.access.user_name': { + category: 'traefik', + name: 'traefik.access.user_name', + type: 'alias', + }, + 'traefik.access.method': { + category: 'traefik', + name: 'traefik.access.method', + type: 'alias', + }, + 'traefik.access.url': { + category: 'traefik', + name: 'traefik.access.url', + type: 'alias', + }, + 'traefik.access.http_version': { + category: 'traefik', + name: 'traefik.access.http_version', + type: 'alias', + }, + 'traefik.access.response_code': { + category: 'traefik', + name: 'traefik.access.response_code', + type: 'alias', + }, + 'traefik.access.referrer': { + category: 'traefik', + name: 'traefik.access.referrer', + type: 'alias', + }, + 'traefik.access.agent': { + category: 'traefik', + name: 'traefik.access.agent', + type: 'alias', + }, + 'traefik.access.user_agent.device': { + category: 'traefik', + name: 'traefik.access.user_agent.device', + type: 'alias', + }, + 'traefik.access.user_agent.name': { + category: 'traefik', + name: 'traefik.access.user_agent.name', + type: 'alias', + }, + 'traefik.access.user_agent.os': { + category: 'traefik', + name: 'traefik.access.user_agent.os', + type: 'alias', + }, + 'traefik.access.user_agent.os_name': { + category: 'traefik', + name: 'traefik.access.user_agent.os_name', + type: 'alias', + }, + 'traefik.access.user_agent.original': { + category: 'traefik', + name: 'traefik.access.user_agent.original', + type: 'alias', + }, + 'traefik.access.geoip.continent_name': { + category: 'traefik', + name: 'traefik.access.geoip.continent_name', + type: 'alias', + }, + 'traefik.access.geoip.country_iso_code': { + category: 'traefik', + name: 'traefik.access.geoip.country_iso_code', + type: 'alias', + }, + 'traefik.access.geoip.location': { + category: 'traefik', + name: 'traefik.access.geoip.location', + type: 'alias', + }, + 'traefik.access.geoip.region_name': { + category: 'traefik', + name: 'traefik.access.geoip.region_name', + type: 'alias', + }, + 'traefik.access.geoip.city_name': { + category: 'traefik', + name: 'traefik.access.geoip.city_name', + type: 'alias', + }, + 'traefik.access.geoip.region_iso_code': { + category: 'traefik', + name: 'traefik.access.geoip.region_iso_code', + type: 'alias', + }, + 'activemq.caller': { + category: 'activemq', + description: 'Name of the caller issuing the logging request (class or resource). ', + name: 'activemq.caller', + type: 'keyword', + }, + 'activemq.thread': { + category: 'activemq', + description: 'Thread that generated the logging event. ', + name: 'activemq.thread', + type: 'keyword', + }, + 'activemq.user': { + category: 'activemq', + description: 'User that generated the logging event. ', + name: 'activemq.user', + type: 'keyword', + }, + 'activemq.audit': { + category: 'activemq', + description: 'Fields from ActiveMQ audit logs. ', + name: 'activemq.audit', + type: 'group', + }, + 'activemq.log.stack_trace': { + category: 'activemq', + name: 'activemq.log.stack_trace', + type: 'keyword', + }, + 'aws.cloudtrail.event_version': { + category: 'aws', + description: 'The CloudTrail version of the log event format. ', + name: 'aws.cloudtrail.event_version', + type: 'keyword', + }, + 'aws.cloudtrail.user_identity.type': { + category: 'aws', + description: 'The type of the identity ', + name: 'aws.cloudtrail.user_identity.type', + type: 'keyword', + }, + 'aws.cloudtrail.user_identity.arn': { + category: 'aws', + description: 'The Amazon Resource Name (ARN) of the principal that made the call.', + name: 'aws.cloudtrail.user_identity.arn', + type: 'keyword', + }, + 'aws.cloudtrail.user_identity.access_key_id': { + category: 'aws', + description: 'The access key ID that was used to sign the request.', + name: 'aws.cloudtrail.user_identity.access_key_id', + type: 'keyword', + }, + 'aws.cloudtrail.user_identity.session_context.mfa_authenticated': { + category: 'aws', + description: + 'The value is true if the root user or IAM user whose credentials were used for the request also was authenticated with an MFA device; otherwise, false.', + name: 'aws.cloudtrail.user_identity.session_context.mfa_authenticated', + type: 'keyword', + }, + 'aws.cloudtrail.user_identity.session_context.creation_date': { + category: 'aws', + description: 'The date and time when the temporary security credentials were issued.', + name: 'aws.cloudtrail.user_identity.session_context.creation_date', + type: 'date', + }, + 'aws.cloudtrail.user_identity.session_context.session_issuer.type': { + category: 'aws', + description: + 'The source of the temporary security credentials, such as Root, IAMUser, or Role.', + name: 'aws.cloudtrail.user_identity.session_context.session_issuer.type', + type: 'keyword', + }, + 'aws.cloudtrail.user_identity.session_context.session_issuer.principal_id': { + category: 'aws', + description: 'The internal ID of the entity that was used to get credentials.', + name: 'aws.cloudtrail.user_identity.session_context.session_issuer.principal_id', + type: 'keyword', + }, + 'aws.cloudtrail.user_identity.session_context.session_issuer.arn': { + category: 'aws', + description: + 'The ARN of the source (account, IAM user, or role) that was used to get temporary security credentials.', + name: 'aws.cloudtrail.user_identity.session_context.session_issuer.arn', + type: 'keyword', + }, + 'aws.cloudtrail.user_identity.session_context.session_issuer.account_id': { + category: 'aws', + description: 'The account that owns the entity that was used to get credentials.', + name: 'aws.cloudtrail.user_identity.session_context.session_issuer.account_id', + type: 'keyword', + }, + 'aws.cloudtrail.user_identity.invoked_by': { + category: 'aws', + description: + 'The name of the AWS service that made the request, such as Amazon EC2 Auto Scaling or AWS Elastic Beanstalk.', + name: 'aws.cloudtrail.user_identity.invoked_by', + type: 'keyword', + }, + 'aws.cloudtrail.error_code': { + category: 'aws', + description: 'The AWS service error if the request returns an error.', + name: 'aws.cloudtrail.error_code', + type: 'keyword', + }, + 'aws.cloudtrail.error_message': { + category: 'aws', + description: 'If the request returns an error, the description of the error.', + name: 'aws.cloudtrail.error_message', + type: 'keyword', + }, + 'aws.cloudtrail.request_parameters': { + category: 'aws', + description: 'The parameters, if any, that were sent with the request.', + name: 'aws.cloudtrail.request_parameters', + type: 'keyword', + }, + 'aws.cloudtrail.response_elements': { + category: 'aws', + description: + 'The response element for actions that make changes (create, update, or delete actions).', + name: 'aws.cloudtrail.response_elements', + type: 'keyword', + }, + 'aws.cloudtrail.additional_eventdata': { + category: 'aws', + description: 'Additional data about the event that was not part of the request or response.', + name: 'aws.cloudtrail.additional_eventdata', + type: 'keyword', + }, + 'aws.cloudtrail.request_id': { + category: 'aws', + description: + 'The value that identifies the request. The service being called generates this value.', + name: 'aws.cloudtrail.request_id', + type: 'keyword', + }, + 'aws.cloudtrail.event_type': { + category: 'aws', + description: 'Identifies the type of event that generated the event record.', + name: 'aws.cloudtrail.event_type', + type: 'keyword', + }, + 'aws.cloudtrail.api_version': { + category: 'aws', + description: 'Identifies the API version associated with the AwsApiCall eventType value.', + name: 'aws.cloudtrail.api_version', + type: 'keyword', + }, + 'aws.cloudtrail.management_event': { + category: 'aws', + description: 'A Boolean value that identifies whether the event is a management event.', + name: 'aws.cloudtrail.management_event', + type: 'keyword', + }, + 'aws.cloudtrail.read_only': { + category: 'aws', + description: 'Identifies whether this operation is a read-only operation.', + name: 'aws.cloudtrail.read_only', + type: 'keyword', + }, + 'aws.cloudtrail.resources.arn': { + category: 'aws', + description: 'Resource ARNs', + name: 'aws.cloudtrail.resources.arn', + type: 'keyword', + }, + 'aws.cloudtrail.resources.account_id': { + category: 'aws', + description: 'Account ID of the resource owner', + name: 'aws.cloudtrail.resources.account_id', + type: 'keyword', + }, + 'aws.cloudtrail.resources.type': { + category: 'aws', + description: 'Resource type identifier in the format: AWS::aws-service-name::data-type-name', + name: 'aws.cloudtrail.resources.type', + type: 'keyword', + }, + 'aws.cloudtrail.recipient_account_id': { + category: 'aws', + description: 'Represents the account ID that received this event.', + name: 'aws.cloudtrail.recipient_account_id', + type: 'keyword', + }, + 'aws.cloudtrail.service_event_details': { + category: 'aws', + description: 'Identifies the service event, including what triggered the event and the result.', + name: 'aws.cloudtrail.service_event_details', + type: 'keyword', + }, + 'aws.cloudtrail.shared_event_id': { + category: 'aws', + description: + 'GUID generated by CloudTrail to uniquely identify CloudTrail events from the same AWS action that is sent to different AWS accounts.', + name: 'aws.cloudtrail.shared_event_id', + type: 'keyword', + }, + 'aws.cloudtrail.vpc_endpoint_id': { + category: 'aws', + description: + 'Identifies the VPC endpoint in which requests were made from a VPC to another AWS service, such as Amazon S3.', + name: 'aws.cloudtrail.vpc_endpoint_id', + type: 'keyword', + }, + 'aws.cloudtrail.console_login.additional_eventdata.mobile_version': { + category: 'aws', + description: 'Identifies whether ConsoleLogin was from mobile version', + name: 'aws.cloudtrail.console_login.additional_eventdata.mobile_version', + type: 'boolean', + }, + 'aws.cloudtrail.console_login.additional_eventdata.login_to': { + category: 'aws', + description: 'URL for ConsoleLogin', + name: 'aws.cloudtrail.console_login.additional_eventdata.login_to', + type: 'keyword', + }, + 'aws.cloudtrail.console_login.additional_eventdata.mfa_used': { + category: 'aws', + description: 'Identifies whether multi factor authentication was used during ConsoleLogin', + name: 'aws.cloudtrail.console_login.additional_eventdata.mfa_used', + type: 'boolean', + }, + 'aws.cloudtrail.flattened.additional_eventdata': { + category: 'aws', + description: 'Additional data about the event that was not part of the request or response. ', + name: 'aws.cloudtrail.flattened.additional_eventdata', + type: 'flattened', + }, + 'aws.cloudtrail.flattened.request_parameters': { + category: 'aws', + description: 'The parameters, if any, that were sent with the request.', + name: 'aws.cloudtrail.flattened.request_parameters', + type: 'flattened', + }, + 'aws.cloudtrail.flattened.response_elements': { + category: 'aws', + description: + 'The response element for actions that make changes (create, update, or delete actions).', + name: 'aws.cloudtrail.flattened.response_elements', + type: 'flattened', + }, + 'aws.cloudtrail.flattened.service_event_details': { + category: 'aws', + description: 'Identifies the service event, including what triggered the event and the result.', + name: 'aws.cloudtrail.flattened.service_event_details', + type: 'flattened', + }, + 'aws.cloudwatch.message': { + category: 'aws', + description: 'CloudWatch log message. ', + name: 'aws.cloudwatch.message', + type: 'text', + }, + 'aws.ec2.ip_address': { + category: 'aws', + description: 'The internet address of the requester. ', + name: 'aws.ec2.ip_address', + type: 'keyword', + }, + 'aws.elb.name': { + category: 'aws', + description: 'The name of the load balancer. ', + name: 'aws.elb.name', + type: 'keyword', + }, + 'aws.elb.type': { + category: 'aws', + description: 'The type of the load balancer for v2 Load Balancers. ', + name: 'aws.elb.type', + type: 'keyword', + }, + 'aws.elb.target_group.arn': { + category: 'aws', + description: 'The ARN of the target group handling the request. ', + name: 'aws.elb.target_group.arn', + type: 'keyword', + }, + 'aws.elb.listener': { + category: 'aws', + description: 'The ELB listener that received the connection. ', + name: 'aws.elb.listener', + type: 'keyword', + }, + 'aws.elb.protocol': { + category: 'aws', + description: 'The protocol of the load balancer (http or tcp). ', + name: 'aws.elb.protocol', + type: 'keyword', + }, + 'aws.elb.request_processing_time.sec': { + category: 'aws', + description: + 'The total time in seconds since the connection or request is received until it is sent to a registered backend. ', + name: 'aws.elb.request_processing_time.sec', + type: 'float', + }, + 'aws.elb.backend_processing_time.sec': { + category: 'aws', + description: + 'The total time in seconds since the connection is sent to the backend till the backend starts responding. ', + name: 'aws.elb.backend_processing_time.sec', + type: 'float', + }, + 'aws.elb.response_processing_time.sec': { + category: 'aws', + description: + 'The total time in seconds since the response is received from the backend till it is sent to the client. ', + name: 'aws.elb.response_processing_time.sec', + type: 'float', + }, + 'aws.elb.connection_time.ms': { + category: 'aws', + description: + 'The total time of the connection in milliseconds, since it is opened till it is closed. ', + name: 'aws.elb.connection_time.ms', + type: 'long', + }, + 'aws.elb.tls_handshake_time.ms': { + category: 'aws', + description: + 'The total time for the TLS handshake to complete in milliseconds once the connection has been established. ', + name: 'aws.elb.tls_handshake_time.ms', + type: 'long', + }, + 'aws.elb.backend.ip': { + category: 'aws', + description: 'The IP address of the backend processing this connection. ', + name: 'aws.elb.backend.ip', + type: 'keyword', + }, + 'aws.elb.backend.port': { + category: 'aws', + description: 'The port in the backend processing this connection. ', + name: 'aws.elb.backend.port', + type: 'keyword', + }, + 'aws.elb.backend.http.response.status_code': { + category: 'aws', + description: + 'The status code from the backend (status code sent to the client from ELB is stored in `http.response.status_code` ', + name: 'aws.elb.backend.http.response.status_code', + type: 'keyword', + }, + 'aws.elb.ssl_cipher': { + category: 'aws', + description: 'The SSL cipher used in TLS/SSL connections. ', + name: 'aws.elb.ssl_cipher', + type: 'keyword', + }, + 'aws.elb.ssl_protocol': { + category: 'aws', + description: 'The SSL protocol used in TLS/SSL connections. ', + name: 'aws.elb.ssl_protocol', + type: 'keyword', + }, + 'aws.elb.chosen_cert.arn': { + category: 'aws', + description: + 'The ARN of the chosen certificate presented to the client in TLS/SSL connections. ', + name: 'aws.elb.chosen_cert.arn', + type: 'keyword', + }, + 'aws.elb.chosen_cert.serial': { + category: 'aws', + description: + 'The serial number of the chosen certificate presented to the client in TLS/SSL connections. ', + name: 'aws.elb.chosen_cert.serial', + type: 'keyword', + }, + 'aws.elb.incoming_tls_alert': { + category: 'aws', + description: + 'The integer value of TLS alerts received by the load balancer from the client, if present. ', + name: 'aws.elb.incoming_tls_alert', + type: 'keyword', + }, + 'aws.elb.tls_named_group': { + category: 'aws', + description: 'The TLS named group. ', + name: 'aws.elb.tls_named_group', + type: 'keyword', + }, + 'aws.elb.trace_id': { + category: 'aws', + description: 'The contents of the `X-Amzn-Trace-Id` header. ', + name: 'aws.elb.trace_id', + type: 'keyword', + }, + 'aws.elb.matched_rule_priority': { + category: 'aws', + description: 'The priority value of the rule that matched the request, if a rule matched. ', + name: 'aws.elb.matched_rule_priority', + type: 'keyword', + }, + 'aws.elb.action_executed': { + category: 'aws', + description: + 'The action executed when processing the request (forward, fixed-response, authenticate...). It can contain several values. ', + name: 'aws.elb.action_executed', + type: 'keyword', + }, + 'aws.elb.redirect_url': { + category: 'aws', + description: 'The URL used if a redirection action was executed. ', + name: 'aws.elb.redirect_url', + type: 'keyword', + }, + 'aws.elb.error.reason': { + category: 'aws', + description: 'The error reason if the executed action failed. ', + name: 'aws.elb.error.reason', + type: 'keyword', + }, + 'aws.s3access.bucket_owner': { + category: 'aws', + description: 'The canonical user ID of the owner of the source bucket. ', + name: 'aws.s3access.bucket_owner', + type: 'keyword', + }, + 'aws.s3access.bucket': { + category: 'aws', + description: 'The name of the bucket that the request was processed against. ', + name: 'aws.s3access.bucket', + type: 'keyword', + }, + 'aws.s3access.remote_ip': { + category: 'aws', + description: 'The apparent internet address of the requester. ', + name: 'aws.s3access.remote_ip', + type: 'ip', + }, + 'aws.s3access.requester': { + category: 'aws', + description: 'The canonical user ID of the requester, or a - for unauthenticated requests. ', + name: 'aws.s3access.requester', + type: 'keyword', + }, + 'aws.s3access.request_id': { + category: 'aws', + description: 'A string generated by Amazon S3 to uniquely identify each request. ', + name: 'aws.s3access.request_id', + type: 'keyword', + }, + 'aws.s3access.operation': { + category: 'aws', + description: + 'The operation listed here is declared as SOAP.operation, REST.HTTP_method.resource_type, WEBSITE.HTTP_method.resource_type, or BATCH.DELETE.OBJECT. ', + name: 'aws.s3access.operation', + type: 'keyword', + }, + 'aws.s3access.key': { + category: 'aws', + description: + 'The "key" part of the request, URL encoded, or "-" if the operation does not take a key parameter. ', + name: 'aws.s3access.key', + type: 'keyword', + }, + 'aws.s3access.request_uri': { + category: 'aws', + description: 'The Request-URI part of the HTTP request message. ', + name: 'aws.s3access.request_uri', + type: 'keyword', + }, + 'aws.s3access.http_status': { + category: 'aws', + description: 'The numeric HTTP status code of the response. ', + name: 'aws.s3access.http_status', + type: 'long', + }, + 'aws.s3access.error_code': { + category: 'aws', + description: 'The Amazon S3 Error Code, or "-" if no error occurred. ', + name: 'aws.s3access.error_code', + type: 'keyword', + }, + 'aws.s3access.bytes_sent': { + category: 'aws', + description: + 'The number of response bytes sent, excluding HTTP protocol overhead, or "-" if zero. ', + name: 'aws.s3access.bytes_sent', + type: 'long', + }, + 'aws.s3access.object_size': { + category: 'aws', + description: 'The total size of the object in question. ', + name: 'aws.s3access.object_size', + type: 'long', + }, + 'aws.s3access.total_time': { + category: 'aws', + description: + "The number of milliseconds the request was in flight from the server's perspective. ", + name: 'aws.s3access.total_time', + type: 'long', + }, + 'aws.s3access.turn_around_time': { + category: 'aws', + description: 'The number of milliseconds that Amazon S3 spent processing your request. ', + name: 'aws.s3access.turn_around_time', + type: 'long', + }, + 'aws.s3access.referrer': { + category: 'aws', + description: 'The value of the HTTP Referrer header, if present. ', + name: 'aws.s3access.referrer', + type: 'keyword', + }, + 'aws.s3access.user_agent': { + category: 'aws', + description: 'The value of the HTTP User-Agent header. ', + name: 'aws.s3access.user_agent', + type: 'keyword', + }, + 'aws.s3access.version_id': { + category: 'aws', + description: + 'The version ID in the request, or "-" if the operation does not take a versionId parameter. ', + name: 'aws.s3access.version_id', + type: 'keyword', + }, + 'aws.s3access.host_id': { + category: 'aws', + description: 'The x-amz-id-2 or Amazon S3 extended request ID. ', + name: 'aws.s3access.host_id', + type: 'keyword', + }, + 'aws.s3access.signature_version': { + category: 'aws', + description: + 'The signature version, SigV2 or SigV4, that was used to authenticate the request or a - for unauthenticated requests. ', + name: 'aws.s3access.signature_version', + type: 'keyword', + }, + 'aws.s3access.cipher_suite': { + category: 'aws', + description: + 'The Secure Sockets Layer (SSL) cipher that was negotiated for HTTPS request or a - for HTTP. ', + name: 'aws.s3access.cipher_suite', + type: 'keyword', + }, + 'aws.s3access.authentication_type': { + category: 'aws', + description: + 'The type of request authentication used, AuthHeader for authentication headers, QueryString for query string (pre-signed URL) or a - for unauthenticated requests. ', + name: 'aws.s3access.authentication_type', + type: 'keyword', + }, + 'aws.s3access.host_header': { + category: 'aws', + description: 'The endpoint used to connect to Amazon S3. ', + name: 'aws.s3access.host_header', + type: 'keyword', + }, + 'aws.s3access.tls_version': { + category: 'aws', + description: 'The Transport Layer Security (TLS) version negotiated by the client. ', + name: 'aws.s3access.tls_version', + type: 'keyword', + }, + 'aws.vpcflow.version': { + category: 'aws', + description: + 'The VPC Flow Logs version. If you use the default format, the version is 2. If you specify a custom format, the version is 3. ', + name: 'aws.vpcflow.version', + type: 'keyword', + }, + 'aws.vpcflow.account_id': { + category: 'aws', + description: 'The AWS account ID for the flow log. ', + name: 'aws.vpcflow.account_id', + type: 'keyword', + }, + 'aws.vpcflow.interface_id': { + category: 'aws', + description: 'The ID of the network interface for which the traffic is recorded. ', + name: 'aws.vpcflow.interface_id', + type: 'keyword', + }, + 'aws.vpcflow.action': { + category: 'aws', + description: 'The action that is associated with the traffic, ACCEPT or REJECT. ', + name: 'aws.vpcflow.action', + type: 'keyword', + }, + 'aws.vpcflow.log_status': { + category: 'aws', + description: 'The logging status of the flow log, OK, NODATA or SKIPDATA. ', + name: 'aws.vpcflow.log_status', + type: 'keyword', + }, + 'aws.vpcflow.instance_id': { + category: 'aws', + description: + "The ID of the instance that's associated with network interface for which the traffic is recorded, if the instance is owned by you. ", + name: 'aws.vpcflow.instance_id', + type: 'keyword', + }, + 'aws.vpcflow.pkt_srcaddr': { + category: 'aws', + description: 'The packet-level (original) source IP address of the traffic. ', + name: 'aws.vpcflow.pkt_srcaddr', + type: 'ip', + }, + 'aws.vpcflow.pkt_dstaddr': { + category: 'aws', + description: 'The packet-level (original) destination IP address for the traffic. ', + name: 'aws.vpcflow.pkt_dstaddr', + type: 'ip', + }, + 'aws.vpcflow.vpc_id': { + category: 'aws', + description: + 'The ID of the VPC that contains the network interface for which the traffic is recorded. ', + name: 'aws.vpcflow.vpc_id', + type: 'keyword', + }, + 'aws.vpcflow.subnet_id': { + category: 'aws', + description: + 'The ID of the subnet that contains the network interface for which the traffic is recorded. ', + name: 'aws.vpcflow.subnet_id', + type: 'keyword', + }, + 'aws.vpcflow.tcp_flags': { + category: 'aws', + description: 'The bitmask value for the following TCP flags: 2=SYN,18=SYN-ACK,1=FIN,4=RST ', + name: 'aws.vpcflow.tcp_flags', + type: 'keyword', + }, + 'aws.vpcflow.type': { + category: 'aws', + description: 'The type of traffic: IPv4, IPv6, or EFA. ', + name: 'aws.vpcflow.type', + type: 'keyword', + }, + 'azure.subscription_id': { + category: 'azure', + description: 'Azure subscription ID ', + name: 'azure.subscription_id', + type: 'keyword', + }, + 'azure.correlation_id': { + category: 'azure', + description: 'Correlation ID ', + name: 'azure.correlation_id', + type: 'keyword', + }, + 'azure.tenant_id': { + category: 'azure', + description: 'tenant ID ', + name: 'azure.tenant_id', + type: 'keyword', + }, + 'azure.resource.id': { + category: 'azure', + description: 'Resource ID ', + name: 'azure.resource.id', + type: 'keyword', + }, + 'azure.resource.group': { + category: 'azure', + description: 'Resource group ', + name: 'azure.resource.group', + type: 'keyword', + }, + 'azure.resource.provider': { + category: 'azure', + description: 'Resource type/namespace ', + name: 'azure.resource.provider', + type: 'keyword', + }, + 'azure.resource.namespace': { + category: 'azure', + description: 'Resource type/namespace ', + name: 'azure.resource.namespace', + type: 'keyword', + }, + 'azure.resource.name': { + category: 'azure', + description: 'Name ', + name: 'azure.resource.name', + type: 'keyword', + }, + 'azure.resource.authorization_rule': { + category: 'azure', + description: 'Authorization rule ', + name: 'azure.resource.authorization_rule', + type: 'keyword', + }, + 'azure.activitylogs.identity.claims_initiated_by_user.name': { + category: 'azure', + description: 'Name ', + name: 'azure.activitylogs.identity.claims_initiated_by_user.name', + type: 'keyword', + }, + 'azure.activitylogs.identity.claims_initiated_by_user.givenname': { + category: 'azure', + description: 'Givenname ', + name: 'azure.activitylogs.identity.claims_initiated_by_user.givenname', + type: 'keyword', + }, + 'azure.activitylogs.identity.claims_initiated_by_user.surname': { + category: 'azure', + description: 'Surname ', + name: 'azure.activitylogs.identity.claims_initiated_by_user.surname', + type: 'keyword', + }, + 'azure.activitylogs.identity.claims_initiated_by_user.fullname': { + category: 'azure', + description: 'Fullname ', + name: 'azure.activitylogs.identity.claims_initiated_by_user.fullname', + type: 'keyword', + }, + 'azure.activitylogs.identity.claims_initiated_by_user.schema': { + category: 'azure', + description: 'Schema ', + name: 'azure.activitylogs.identity.claims_initiated_by_user.schema', + type: 'keyword', + }, + 'azure.activitylogs.identity.claims.*': { + category: 'azure', + description: 'Claims ', + name: 'azure.activitylogs.identity.claims.*', + type: 'object', + }, + 'azure.activitylogs.identity.authorization.scope': { + category: 'azure', + description: 'Scope ', + name: 'azure.activitylogs.identity.authorization.scope', + type: 'keyword', + }, + 'azure.activitylogs.identity.authorization.action': { + category: 'azure', + description: 'Action ', + name: 'azure.activitylogs.identity.authorization.action', + type: 'keyword', + }, + 'azure.activitylogs.identity.authorization.evidence.role_assignment_scope': { + category: 'azure', + description: 'Role assignment scope ', + name: 'azure.activitylogs.identity.authorization.evidence.role_assignment_scope', + type: 'keyword', + }, + 'azure.activitylogs.identity.authorization.evidence.role_definition_id': { + category: 'azure', + description: 'Role definition ID ', + name: 'azure.activitylogs.identity.authorization.evidence.role_definition_id', + type: 'keyword', + }, + 'azure.activitylogs.identity.authorization.evidence.role': { + category: 'azure', + description: 'Role ', + name: 'azure.activitylogs.identity.authorization.evidence.role', + type: 'keyword', + }, + 'azure.activitylogs.identity.authorization.evidence.role_assignment_id': { + category: 'azure', + description: 'Role assignment ID ', + name: 'azure.activitylogs.identity.authorization.evidence.role_assignment_id', + type: 'keyword', + }, + 'azure.activitylogs.identity.authorization.evidence.principal_id': { + category: 'azure', + description: 'Principal ID ', + name: 'azure.activitylogs.identity.authorization.evidence.principal_id', + type: 'keyword', + }, + 'azure.activitylogs.identity.authorization.evidence.principal_type': { + category: 'azure', + description: 'Principal type ', + name: 'azure.activitylogs.identity.authorization.evidence.principal_type', + type: 'keyword', + }, + 'azure.activitylogs.operation_name': { + category: 'azure', + description: 'Operation name ', + name: 'azure.activitylogs.operation_name', + type: 'keyword', + }, + 'azure.activitylogs.result_type': { + category: 'azure', + description: 'Result type ', + name: 'azure.activitylogs.result_type', + type: 'keyword', + }, + 'azure.activitylogs.result_signature': { + category: 'azure', + description: 'Result signature ', + name: 'azure.activitylogs.result_signature', + type: 'keyword', + }, + 'azure.activitylogs.category': { + category: 'azure', + description: 'Category ', + name: 'azure.activitylogs.category', + type: 'keyword', + }, + 'azure.activitylogs.event_category': { + category: 'azure', + description: 'Event Category ', + name: 'azure.activitylogs.event_category', + type: 'keyword', + }, + 'azure.activitylogs.properties.service_request_id': { + category: 'azure', + description: 'Service Request Id ', + name: 'azure.activitylogs.properties.service_request_id', + type: 'keyword', + }, + 'azure.activitylogs.properties.status_code': { + category: 'azure', + description: 'Status code ', + name: 'azure.activitylogs.properties.status_code', + type: 'keyword', + }, + 'azure.auditlogs.category': { + category: 'azure', + description: 'The category of the operation. Currently, Audit is the only supported value. ', + name: 'azure.auditlogs.category', + type: 'keyword', + }, + 'azure.auditlogs.operation_name': { + category: 'azure', + description: 'The operation name ', + name: 'azure.auditlogs.operation_name', + type: 'keyword', + }, + 'azure.auditlogs.operation_version': { + category: 'azure', + description: 'The operation version ', + name: 'azure.auditlogs.operation_version', + type: 'keyword', + }, + 'azure.auditlogs.identity': { + category: 'azure', + description: 'Identity ', + name: 'azure.auditlogs.identity', + type: 'keyword', + }, + 'azure.auditlogs.tenant_id': { + category: 'azure', + description: 'Tenant ID ', + name: 'azure.auditlogs.tenant_id', + type: 'keyword', + }, + 'azure.auditlogs.result_signature': { + category: 'azure', + description: 'Result signature ', + name: 'azure.auditlogs.result_signature', + type: 'keyword', + }, + 'azure.auditlogs.properties.result': { + category: 'azure', + description: 'Log result ', + name: 'azure.auditlogs.properties.result', + type: 'keyword', + }, + 'azure.auditlogs.properties.activity_display_name': { + category: 'azure', + description: 'Activity display name ', + name: 'azure.auditlogs.properties.activity_display_name', + type: 'keyword', + }, + 'azure.auditlogs.properties.result_reason': { + category: 'azure', + description: 'Reason for the log result ', + name: 'azure.auditlogs.properties.result_reason', + type: 'keyword', + }, + 'azure.auditlogs.properties.correlation_id': { + category: 'azure', + description: 'Correlation ID ', + name: 'azure.auditlogs.properties.correlation_id', + type: 'keyword', + }, + 'azure.auditlogs.properties.logged_by_service': { + category: 'azure', + description: 'Logged by service ', + name: 'azure.auditlogs.properties.logged_by_service', + type: 'keyword', + }, + 'azure.auditlogs.properties.operation_type': { + category: 'azure', + description: 'Operation type ', + name: 'azure.auditlogs.properties.operation_type', + type: 'keyword', + }, + 'azure.auditlogs.properties.id': { + category: 'azure', + description: 'ID ', + name: 'azure.auditlogs.properties.id', + type: 'keyword', + }, + 'azure.auditlogs.properties.activity_datetime': { + category: 'azure', + description: 'Activity timestamp ', + name: 'azure.auditlogs.properties.activity_datetime', + type: 'date', + }, + 'azure.auditlogs.properties.category': { + category: 'azure', + description: 'category ', + name: 'azure.auditlogs.properties.category', + type: 'keyword', + }, + 'azure.auditlogs.properties.target_resources.*.display_name': { + category: 'azure', + description: 'Display name ', + name: 'azure.auditlogs.properties.target_resources.*.display_name', + type: 'keyword', + }, + 'azure.auditlogs.properties.target_resources.*.id': { + category: 'azure', + description: 'ID ', + name: 'azure.auditlogs.properties.target_resources.*.id', + type: 'keyword', + }, + 'azure.auditlogs.properties.target_resources.*.type': { + category: 'azure', + description: 'Type ', + name: 'azure.auditlogs.properties.target_resources.*.type', + type: 'keyword', + }, + 'azure.auditlogs.properties.target_resources.*.ip_address': { + category: 'azure', + description: 'ip Address ', + name: 'azure.auditlogs.properties.target_resources.*.ip_address', + type: 'keyword', + }, + 'azure.auditlogs.properties.target_resources.*.user_principal_name': { + category: 'azure', + description: 'User principal name ', + name: 'azure.auditlogs.properties.target_resources.*.user_principal_name', + type: 'keyword', + }, + 'azure.auditlogs.properties.target_resources.*.modified_properties.*.new_value': { + category: 'azure', + description: 'New value ', + name: 'azure.auditlogs.properties.target_resources.*.modified_properties.*.new_value', + type: 'keyword', + }, + 'azure.auditlogs.properties.target_resources.*.modified_properties.*.display_name': { + category: 'azure', + description: 'Display value ', + name: 'azure.auditlogs.properties.target_resources.*.modified_properties.*.display_name', + type: 'keyword', + }, + 'azure.auditlogs.properties.target_resources.*.modified_properties.*.old_value': { + category: 'azure', + description: 'Old value ', + name: 'azure.auditlogs.properties.target_resources.*.modified_properties.*.old_value', + type: 'keyword', + }, + 'azure.auditlogs.properties.initiated_by.app.servicePrincipalName': { + category: 'azure', + description: 'Service principal name ', + name: 'azure.auditlogs.properties.initiated_by.app.servicePrincipalName', + type: 'keyword', + }, + 'azure.auditlogs.properties.initiated_by.app.displayName': { + category: 'azure', + description: 'Display name ', + name: 'azure.auditlogs.properties.initiated_by.app.displayName', + type: 'keyword', + }, + 'azure.auditlogs.properties.initiated_by.app.appId': { + category: 'azure', + description: 'App ID ', + name: 'azure.auditlogs.properties.initiated_by.app.appId', + type: 'keyword', + }, + 'azure.auditlogs.properties.initiated_by.app.servicePrincipalId': { + category: 'azure', + description: 'Service principal ID ', + name: 'azure.auditlogs.properties.initiated_by.app.servicePrincipalId', + type: 'keyword', + }, + 'azure.auditlogs.properties.initiated_by.user.userPrincipalName': { + category: 'azure', + description: 'User principal name ', + name: 'azure.auditlogs.properties.initiated_by.user.userPrincipalName', + type: 'keyword', + }, + 'azure.auditlogs.properties.initiated_by.user.displayName': { + category: 'azure', + description: 'Display name ', + name: 'azure.auditlogs.properties.initiated_by.user.displayName', + type: 'keyword', + }, + 'azure.auditlogs.properties.initiated_by.user.id': { + category: 'azure', + description: 'ID ', + name: 'azure.auditlogs.properties.initiated_by.user.id', + type: 'keyword', + }, + 'azure.auditlogs.properties.initiated_by.user.ipAddress': { + category: 'azure', + description: 'ip Address ', + name: 'azure.auditlogs.properties.initiated_by.user.ipAddress', + type: 'keyword', + }, + 'azure.signinlogs.operation_name': { + category: 'azure', + description: 'The operation name ', + name: 'azure.signinlogs.operation_name', + type: 'keyword', + }, + 'azure.signinlogs.operation_version': { + category: 'azure', + description: 'The operation version ', + name: 'azure.signinlogs.operation_version', + type: 'keyword', + }, + 'azure.signinlogs.tenant_id': { + category: 'azure', + description: 'Tenant ID ', + name: 'azure.signinlogs.tenant_id', + type: 'keyword', + }, + 'azure.signinlogs.result_signature': { + category: 'azure', + description: 'Result signature ', + name: 'azure.signinlogs.result_signature', + type: 'keyword', + }, + 'azure.signinlogs.result_description': { + category: 'azure', + description: 'Result description ', + name: 'azure.signinlogs.result_description', + type: 'keyword', + }, + 'azure.signinlogs.result_type': { + category: 'azure', + description: 'Result type ', + name: 'azure.signinlogs.result_type', + type: 'keyword', + }, + 'azure.signinlogs.identity': { + category: 'azure', + description: 'Identity ', + name: 'azure.signinlogs.identity', + type: 'keyword', + }, + 'azure.signinlogs.category': { + category: 'azure', + description: 'Category ', + name: 'azure.signinlogs.category', + type: 'keyword', + }, + 'azure.signinlogs.properties.id': { + category: 'azure', + description: 'ID ', + name: 'azure.signinlogs.properties.id', + type: 'keyword', + }, + 'azure.signinlogs.properties.created_at': { + category: 'azure', + description: 'Created date time ', + name: 'azure.signinlogs.properties.created_at', + type: 'date', + }, + 'azure.signinlogs.properties.user_display_name': { + category: 'azure', + description: 'User display name ', + name: 'azure.signinlogs.properties.user_display_name', + type: 'keyword', + }, + 'azure.signinlogs.properties.correlation_id': { + category: 'azure', + description: 'Correlation ID ', + name: 'azure.signinlogs.properties.correlation_id', + type: 'keyword', + }, + 'azure.signinlogs.properties.user_principal_name': { + category: 'azure', + description: 'User principal name ', + name: 'azure.signinlogs.properties.user_principal_name', + type: 'keyword', + }, + 'azure.signinlogs.properties.user_id': { + category: 'azure', + description: 'User ID ', + name: 'azure.signinlogs.properties.user_id', + type: 'keyword', + }, + 'azure.signinlogs.properties.app_id': { + category: 'azure', + description: 'App ID ', + name: 'azure.signinlogs.properties.app_id', + type: 'keyword', + }, + 'azure.signinlogs.properties.app_display_name': { + category: 'azure', + description: 'App display name ', + name: 'azure.signinlogs.properties.app_display_name', + type: 'keyword', + }, + 'azure.signinlogs.properties.ip_address': { + category: 'azure', + description: 'Ip address ', + name: 'azure.signinlogs.properties.ip_address', + type: 'keyword', + }, + 'azure.signinlogs.properties.client_app_used': { + category: 'azure', + description: 'Client app used ', + name: 'azure.signinlogs.properties.client_app_used', + type: 'keyword', + }, + 'azure.signinlogs.properties.conditional_access_status': { + category: 'azure', + description: 'Conditional access status ', + name: 'azure.signinlogs.properties.conditional_access_status', + type: 'keyword', + }, + 'azure.signinlogs.properties.original_request_id': { + category: 'azure', + description: 'Original request ID ', + name: 'azure.signinlogs.properties.original_request_id', + type: 'keyword', + }, + 'azure.signinlogs.properties.is_interactive': { + category: 'azure', + description: 'Is interactive ', + name: 'azure.signinlogs.properties.is_interactive', + type: 'keyword', + }, + 'azure.signinlogs.properties.token_issuer_name': { + category: 'azure', + description: 'Token issuer name ', + name: 'azure.signinlogs.properties.token_issuer_name', + type: 'keyword', + }, + 'azure.signinlogs.properties.token_issuer_type': { + category: 'azure', + description: 'Token issuer type ', + name: 'azure.signinlogs.properties.token_issuer_type', + type: 'keyword', + }, + 'azure.signinlogs.properties.processing_time_ms': { + category: 'azure', + description: 'Processing time in milliseconds ', + name: 'azure.signinlogs.properties.processing_time_ms', + type: 'float', + }, + 'azure.signinlogs.properties.risk_detail': { + category: 'azure', + description: 'Risk detail ', + name: 'azure.signinlogs.properties.risk_detail', + type: 'keyword', + }, + 'azure.signinlogs.properties.risk_level_aggregated': { + category: 'azure', + description: 'Risk level aggregated ', + name: 'azure.signinlogs.properties.risk_level_aggregated', + type: 'keyword', + }, + 'azure.signinlogs.properties.risk_level_during_signin': { + category: 'azure', + description: 'Risk level during signIn ', + name: 'azure.signinlogs.properties.risk_level_during_signin', + type: 'keyword', + }, + 'azure.signinlogs.properties.risk_state': { + category: 'azure', + description: 'Risk state ', + name: 'azure.signinlogs.properties.risk_state', + type: 'keyword', + }, + 'azure.signinlogs.properties.resource_display_name': { + category: 'azure', + description: 'Resource display name ', + name: 'azure.signinlogs.properties.resource_display_name', + type: 'keyword', + }, + 'azure.signinlogs.properties.status.error_code': { + category: 'azure', + description: 'Error code ', + name: 'azure.signinlogs.properties.status.error_code', + type: 'keyword', + }, + 'azure.signinlogs.properties.device_detail.device_id': { + category: 'azure', + description: 'Device ID ', + name: 'azure.signinlogs.properties.device_detail.device_id', + type: 'keyword', + }, + 'azure.signinlogs.properties.device_detail.operating_system': { + category: 'azure', + description: 'Operating system ', + name: 'azure.signinlogs.properties.device_detail.operating_system', + type: 'keyword', + }, + 'azure.signinlogs.properties.device_detail.browser': { + category: 'azure', + description: 'Browser ', + name: 'azure.signinlogs.properties.device_detail.browser', + type: 'keyword', + }, + 'azure.signinlogs.properties.device_detail.display_name': { + category: 'azure', + description: 'Display name ', + name: 'azure.signinlogs.properties.device_detail.display_name', + type: 'keyword', + }, + 'azure.signinlogs.properties.device_detail.trust_type': { + category: 'azure', + description: 'Trust type ', + name: 'azure.signinlogs.properties.device_detail.trust_type', + type: 'keyword', + }, + 'azure.signinlogs.properties.service_principal_id': { + category: 'azure', + description: 'Status ', + name: 'azure.signinlogs.properties.service_principal_id', + type: 'keyword', + }, + 'network.interface.name': { + category: 'network', + description: 'Name of the network interface where the traffic has been observed. ', + name: 'network.interface.name', + type: 'keyword', + }, + 'rsa.internal.msg': { + category: 'rsa', + description: 'This key is used to capture the raw message that comes into the Log Decoder', + name: 'rsa.internal.msg', + type: 'keyword', + }, + 'rsa.internal.messageid': { + category: 'rsa', + name: 'rsa.internal.messageid', + type: 'keyword', + }, + 'rsa.internal.event_desc': { + category: 'rsa', + name: 'rsa.internal.event_desc', + type: 'keyword', + }, + 'rsa.internal.message': { + category: 'rsa', + description: 'This key captures the contents of instant messages', + name: 'rsa.internal.message', + type: 'keyword', + }, + 'rsa.internal.time': { + category: 'rsa', + description: + 'This is the time at which a session hits a NetWitness Decoder. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness.', + name: 'rsa.internal.time', + type: 'date', + }, + 'rsa.internal.level': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.level', + type: 'long', + }, + 'rsa.internal.msg_id': { + category: 'rsa', + description: + 'This is the Message ID1 value that identifies the exact log parser definition which parses a particular log session. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.msg_id', + type: 'keyword', + }, + 'rsa.internal.msg_vid': { + category: 'rsa', + description: + 'This is the Message ID2 value that identifies the exact log parser definition which parses a particular log session. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.msg_vid', + type: 'keyword', + }, + 'rsa.internal.data': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.data', + type: 'keyword', + }, + 'rsa.internal.obj_server': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.obj_server', + type: 'keyword', + }, + 'rsa.internal.obj_val': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.obj_val', + type: 'keyword', + }, + 'rsa.internal.resource': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.resource', + type: 'keyword', + }, + 'rsa.internal.obj_id': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.obj_id', + type: 'keyword', + }, + 'rsa.internal.statement': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.statement', + type: 'keyword', + }, + 'rsa.internal.audit_class': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.audit_class', + type: 'keyword', + }, + 'rsa.internal.entry': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.entry', + type: 'keyword', + }, + 'rsa.internal.hcode': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.hcode', + type: 'keyword', + }, + 'rsa.internal.inode': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.inode', + type: 'long', + }, + 'rsa.internal.resource_class': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.resource_class', + type: 'keyword', + }, + 'rsa.internal.dead': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.dead', + type: 'long', + }, + 'rsa.internal.feed_desc': { + category: 'rsa', + description: + 'This is used to capture the description of the feed. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.feed_desc', + type: 'keyword', + }, + 'rsa.internal.feed_name': { + category: 'rsa', + description: + 'This is used to capture the name of the feed. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.feed_name', + type: 'keyword', + }, + 'rsa.internal.cid': { + category: 'rsa', + description: + 'This is the unique identifier used to identify a NetWitness Concentrator. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.cid', + type: 'keyword', + }, + 'rsa.internal.device_class': { + category: 'rsa', + description: + 'This is the Classification of the Log Event Source under a predefined fixed set of Event Source Classifications. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.device_class', + type: 'keyword', + }, + 'rsa.internal.device_group': { + category: 'rsa', + description: + 'This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.device_group', + type: 'keyword', + }, + 'rsa.internal.device_host': { + category: 'rsa', + description: + 'This is the Hostname of the log Event Source sending the logs to NetWitness. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.device_host', + type: 'keyword', + }, + 'rsa.internal.device_ip': { + category: 'rsa', + description: + 'This is the IPv4 address of the Log Event Source sending the logs to NetWitness. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.device_ip', + type: 'ip', + }, + 'rsa.internal.device_ipv6': { + category: 'rsa', + description: + 'This is the IPv6 address of the Log Event Source sending the logs to NetWitness. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.device_ipv6', + type: 'ip', + }, + 'rsa.internal.device_type': { + category: 'rsa', + description: + 'This is the name of the log parser which parsed a given session. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.device_type', + type: 'keyword', + }, + 'rsa.internal.device_type_id': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.device_type_id', + type: 'long', + }, + 'rsa.internal.did': { + category: 'rsa', + description: + 'This is the unique identifier used to identify a NetWitness Decoder. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.did', + type: 'keyword', + }, + 'rsa.internal.entropy_req': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, the Meta Type can be either UInt16 or Float32 based on the configuration', + name: 'rsa.internal.entropy_req', + type: 'long', + }, + 'rsa.internal.entropy_res': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, the Meta Type can be either UInt16 or Float32 based on the configuration', + name: 'rsa.internal.entropy_res', + type: 'long', + }, + 'rsa.internal.event_name': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.event_name', + type: 'keyword', + }, + 'rsa.internal.feed_category': { + category: 'rsa', + description: + 'This is used to capture the category of the feed. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.feed_category', + type: 'keyword', + }, + 'rsa.internal.forward_ip': { + category: 'rsa', + description: + 'This key should be used to capture the IPV4 address of a relay system which forwarded the events from the original system to NetWitness.', + name: 'rsa.internal.forward_ip', + type: 'ip', + }, + 'rsa.internal.forward_ipv6': { + category: 'rsa', + description: + 'This key is used to capture the IPV6 address of a relay system which forwarded the events from the original system to NetWitness. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.forward_ipv6', + type: 'ip', + }, + 'rsa.internal.header_id': { + category: 'rsa', + description: + 'This is the Header ID value that identifies the exact log parser header definition that parses a particular log session. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.header_id', + type: 'keyword', + }, + 'rsa.internal.lc_cid': { + category: 'rsa', + description: + 'This is a unique Identifier of a Log Collector. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.lc_cid', + type: 'keyword', + }, + 'rsa.internal.lc_ctime': { + category: 'rsa', + description: + 'This is the time at which a log is collected in a NetWitness Log Collector. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.lc_ctime', + type: 'date', + }, + 'rsa.internal.mcb_req': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, the most common byte request is simply which byte for each side (0 thru 255) was seen the most', + name: 'rsa.internal.mcb_req', + type: 'long', + }, + 'rsa.internal.mcb_res': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, the most common byte response is simply which byte for each side (0 thru 255) was seen the most', + name: 'rsa.internal.mcb_res', + type: 'long', + }, + 'rsa.internal.mcbc_req': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, the most common byte count is the number of times the most common byte (above) was seen in the session streams', + name: 'rsa.internal.mcbc_req', + type: 'long', + }, + 'rsa.internal.mcbc_res': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, the most common byte count is the number of times the most common byte (above) was seen in the session streams', + name: 'rsa.internal.mcbc_res', + type: 'long', + }, + 'rsa.internal.medium': { + category: 'rsa', + description: + 'This key is used to identify if it’s a log/packet session or Layer 2 Encapsulation Type. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness. 32 = log, 33 = correlation session, < 32 is packet session', + name: 'rsa.internal.medium', + type: 'long', + }, + 'rsa.internal.node_name': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.node_name', + type: 'keyword', + }, + 'rsa.internal.nwe_callback_id': { + category: 'rsa', + description: 'This key denotes that event is endpoint related', + name: 'rsa.internal.nwe_callback_id', + type: 'keyword', + }, + 'rsa.internal.parse_error': { + category: 'rsa', + description: + 'This is a special key that stores any Meta key validation error found while parsing a log session. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.parse_error', + type: 'keyword', + }, + 'rsa.internal.payload_req': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, the payload size metrics are the payload sizes of each session side at the time of parsing. However, in order to keep', + name: 'rsa.internal.payload_req', + type: 'long', + }, + 'rsa.internal.payload_res': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, the payload size metrics are the payload sizes of each session side at the time of parsing. However, in order to keep', + name: 'rsa.internal.payload_res', + type: 'long', + }, + 'rsa.internal.process_vid_dst': { + category: 'rsa', + description: + 'Endpoint generates and uses a unique virtual ID to identify any similar group of process. This ID represents the target process.', + name: 'rsa.internal.process_vid_dst', + type: 'keyword', + }, + 'rsa.internal.process_vid_src': { + category: 'rsa', + description: + 'Endpoint generates and uses a unique virtual ID to identify any similar group of process. This ID represents the source process.', + name: 'rsa.internal.process_vid_src', + type: 'keyword', + }, + 'rsa.internal.rid': { + category: 'rsa', + description: + 'This is a special ID of the Remote Session created by NetWitness Decoder. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.rid', + type: 'long', + }, + 'rsa.internal.session_split': { + category: 'rsa', + description: + 'This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.session_split', + type: 'keyword', + }, + 'rsa.internal.site': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.internal.site', + type: 'keyword', + }, + 'rsa.internal.size': { + category: 'rsa', + description: + 'This is the size of the session as seen by the NetWitness Decoder. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.size', + type: 'long', + }, + 'rsa.internal.sourcefile': { + category: 'rsa', + description: + 'This is the name of the log file or PCAPs that can be imported into NetWitness. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.internal.sourcefile', + type: 'keyword', + }, + 'rsa.internal.ubc_req': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, Unique byte count is the number of unique bytes seen in each stream. 256 would mean all byte values of 0 thru 255 were seen at least once', + name: 'rsa.internal.ubc_req', + type: 'long', + }, + 'rsa.internal.ubc_res': { + category: 'rsa', + description: + 'This key is only used by the Entropy Parser, Unique byte count is the number of unique bytes seen in each stream. 256 would mean all byte values of 0 thru 255 were seen at least once', + name: 'rsa.internal.ubc_res', + type: 'long', + }, + 'rsa.internal.word': { + category: 'rsa', + description: + 'This is used by the Word Parsing technology to capture the first 5 character of every word in an unparsed log', + name: 'rsa.internal.word', + type: 'keyword', + }, + 'rsa.time.event_time': { + category: 'rsa', + description: + 'This key is used to capture the time mentioned in a raw session that represents the actual time an event occured in a standard normalized form', + name: 'rsa.time.event_time', + type: 'date', + }, + 'rsa.time.duration_time': { + category: 'rsa', + description: 'This key is used to capture the normalized duration/lifetime in seconds.', + name: 'rsa.time.duration_time', + type: 'double', + }, + 'rsa.time.event_time_str': { + category: 'rsa', + description: + 'This key is used to capture the incomplete time mentioned in a session as a string', + name: 'rsa.time.event_time_str', + type: 'keyword', + }, + 'rsa.time.starttime': { + category: 'rsa', + description: + 'This key is used to capture the Start time mentioned in a session in a standard form', + name: 'rsa.time.starttime', + type: 'date', + }, + 'rsa.time.month': { + category: 'rsa', + name: 'rsa.time.month', + type: 'keyword', + }, + 'rsa.time.day': { + category: 'rsa', + name: 'rsa.time.day', + type: 'keyword', + }, + 'rsa.time.endtime': { + category: 'rsa', + description: + 'This key is used to capture the End time mentioned in a session in a standard form', + name: 'rsa.time.endtime', + type: 'date', + }, + 'rsa.time.timezone': { + category: 'rsa', + description: 'This key is used to capture the timezone of the Event Time', + name: 'rsa.time.timezone', + type: 'keyword', + }, + 'rsa.time.duration_str': { + category: 'rsa', + description: 'A text string version of the duration', + name: 'rsa.time.duration_str', + type: 'keyword', + }, + 'rsa.time.date': { + category: 'rsa', + name: 'rsa.time.date', + type: 'keyword', + }, + 'rsa.time.year': { + category: 'rsa', + name: 'rsa.time.year', + type: 'keyword', + }, + 'rsa.time.recorded_time': { + category: 'rsa', + description: + "The event time as recorded by the system the event is collected from. The usage scenario is a multi-tier application where the management layer of the system records it's own timestamp at the time of collection from its child nodes. Must be in timestamp format.", + name: 'rsa.time.recorded_time', + type: 'date', + }, + 'rsa.time.datetime': { + category: 'rsa', + name: 'rsa.time.datetime', + type: 'keyword', + }, + 'rsa.time.effective_time': { + category: 'rsa', + description: + 'This key is the effective time referenced by an individual event in a Standard Timestamp format', + name: 'rsa.time.effective_time', + type: 'date', + }, + 'rsa.time.expire_time': { + category: 'rsa', + description: 'This key is the timestamp that explicitly refers to an expiration.', + name: 'rsa.time.expire_time', + type: 'date', + }, + 'rsa.time.process_time': { + category: 'rsa', + description: 'Deprecated, use duration.time', + name: 'rsa.time.process_time', + type: 'keyword', + }, + 'rsa.time.hour': { + category: 'rsa', + name: 'rsa.time.hour', + type: 'keyword', + }, + 'rsa.time.min': { + category: 'rsa', + name: 'rsa.time.min', + type: 'keyword', + }, + 'rsa.time.timestamp': { + category: 'rsa', + name: 'rsa.time.timestamp', + type: 'keyword', + }, + 'rsa.time.event_queue_time': { + category: 'rsa', + description: 'This key is the Time that the event was queued.', + name: 'rsa.time.event_queue_time', + type: 'date', + }, + 'rsa.time.p_time1': { + category: 'rsa', + name: 'rsa.time.p_time1', + type: 'keyword', + }, + 'rsa.time.tzone': { + category: 'rsa', + name: 'rsa.time.tzone', + type: 'keyword', + }, + 'rsa.time.eventtime': { + category: 'rsa', + name: 'rsa.time.eventtime', + type: 'keyword', + }, + 'rsa.time.gmtdate': { + category: 'rsa', + name: 'rsa.time.gmtdate', + type: 'keyword', + }, + 'rsa.time.gmttime': { + category: 'rsa', + name: 'rsa.time.gmttime', + type: 'keyword', + }, + 'rsa.time.p_date': { + category: 'rsa', + name: 'rsa.time.p_date', + type: 'keyword', + }, + 'rsa.time.p_month': { + category: 'rsa', + name: 'rsa.time.p_month', + type: 'keyword', + }, + 'rsa.time.p_time': { + category: 'rsa', + name: 'rsa.time.p_time', + type: 'keyword', + }, + 'rsa.time.p_time2': { + category: 'rsa', + name: 'rsa.time.p_time2', + type: 'keyword', + }, + 'rsa.time.p_year': { + category: 'rsa', + name: 'rsa.time.p_year', + type: 'keyword', + }, + 'rsa.time.expire_time_str': { + category: 'rsa', + description: + 'This key is used to capture incomplete timestamp that explicitly refers to an expiration.', + name: 'rsa.time.expire_time_str', + type: 'keyword', + }, + 'rsa.time.stamp': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.time.stamp', + type: 'date', + }, + 'rsa.misc.action': { + category: 'rsa', + name: 'rsa.misc.action', + type: 'keyword', + }, + 'rsa.misc.result': { + category: 'rsa', + description: + 'This key is used to capture the outcome/result string value of an action in a session.', + name: 'rsa.misc.result', + type: 'keyword', + }, + 'rsa.misc.severity': { + category: 'rsa', + description: 'This key is used to capture the severity given the session', + name: 'rsa.misc.severity', + type: 'keyword', + }, + 'rsa.misc.event_type': { + category: 'rsa', + description: 'This key captures the event category type as specified by the event source.', + name: 'rsa.misc.event_type', + type: 'keyword', + }, + 'rsa.misc.reference_id': { + category: 'rsa', + description: 'This key is used to capture an event id from the session directly', + name: 'rsa.misc.reference_id', + type: 'keyword', + }, + 'rsa.misc.version': { + category: 'rsa', + description: + 'This key captures Version of the application or OS which is generating the event.', + name: 'rsa.misc.version', + type: 'keyword', + }, + 'rsa.misc.disposition': { + category: 'rsa', + description: 'This key captures the The end state of an action.', + name: 'rsa.misc.disposition', + type: 'keyword', + }, + 'rsa.misc.result_code': { + category: 'rsa', + description: + 'This key is used to capture the outcome/result numeric value of an action in a session', + name: 'rsa.misc.result_code', + type: 'keyword', + }, + 'rsa.misc.category': { + category: 'rsa', + description: + 'This key is used to capture the category of an event given by the vendor in the session', + name: 'rsa.misc.category', + type: 'keyword', + }, + 'rsa.misc.obj_name': { + category: 'rsa', + description: 'This is used to capture name of object', + name: 'rsa.misc.obj_name', + type: 'keyword', + }, + 'rsa.misc.obj_type': { + category: 'rsa', + description: 'This is used to capture type of object', + name: 'rsa.misc.obj_type', + type: 'keyword', + }, + 'rsa.misc.event_source': { + category: 'rsa', + description: 'This key captures Source of the event that’s not a hostname', + name: 'rsa.misc.event_source', + type: 'keyword', + }, + 'rsa.misc.log_session_id': { + category: 'rsa', + description: 'This key is used to capture a sessionid from the session directly', + name: 'rsa.misc.log_session_id', + type: 'keyword', + }, + 'rsa.misc.group': { + category: 'rsa', + description: 'This key captures the Group Name value', + name: 'rsa.misc.group', + type: 'keyword', + }, + 'rsa.misc.policy_name': { + category: 'rsa', + description: 'This key is used to capture the Policy Name only.', + name: 'rsa.misc.policy_name', + type: 'keyword', + }, + 'rsa.misc.rule_name': { + category: 'rsa', + description: 'This key captures the Rule Name', + name: 'rsa.misc.rule_name', + type: 'keyword', + }, + 'rsa.misc.context': { + category: 'rsa', + description: 'This key captures Information which adds additional context to the event.', + name: 'rsa.misc.context', + type: 'keyword', + }, + 'rsa.misc.change_new': { + category: 'rsa', + description: + 'This key is used to capture the new values of the attribute that’s changing in a session', + name: 'rsa.misc.change_new', + type: 'keyword', + }, + 'rsa.misc.space': { + category: 'rsa', + name: 'rsa.misc.space', + type: 'keyword', + }, + 'rsa.misc.client': { + category: 'rsa', + description: + 'This key is used to capture only the name of the client application requesting resources of the server. See the user.agent meta key for capture of the specific user agent identifier or browser identification string.', + name: 'rsa.misc.client', + type: 'keyword', + }, + 'rsa.misc.msgIdPart1': { + category: 'rsa', + name: 'rsa.misc.msgIdPart1', + type: 'keyword', + }, + 'rsa.misc.msgIdPart2': { + category: 'rsa', + name: 'rsa.misc.msgIdPart2', + type: 'keyword', + }, + 'rsa.misc.change_old': { + category: 'rsa', + description: + 'This key is used to capture the old value of the attribute that’s changing in a session', + name: 'rsa.misc.change_old', + type: 'keyword', + }, + 'rsa.misc.operation_id': { + category: 'rsa', + description: + 'An alert number or operation number. The values should be unique and non-repeating.', + name: 'rsa.misc.operation_id', + type: 'keyword', + }, + 'rsa.misc.event_state': { + category: 'rsa', + description: + 'This key captures the current state of the object/item referenced within the event. Describing an on-going event.', + name: 'rsa.misc.event_state', + type: 'keyword', + }, + 'rsa.misc.group_object': { + category: 'rsa', + description: 'This key captures a collection/grouping of entities. Specific usage', + name: 'rsa.misc.group_object', + type: 'keyword', + }, + 'rsa.misc.node': { + category: 'rsa', + description: + 'Common use case is the node name within a cluster. The cluster name is reflected by the host name.', + name: 'rsa.misc.node', + type: 'keyword', + }, + 'rsa.misc.rule': { + category: 'rsa', + description: 'This key captures the Rule number', + name: 'rsa.misc.rule', + type: 'keyword', + }, + 'rsa.misc.device_name': { + category: 'rsa', + description: + 'This is used to capture name of the Device associated with the node Like: a physical disk, printer, etc', + name: 'rsa.misc.device_name', + type: 'keyword', + }, + 'rsa.misc.param': { + category: 'rsa', + description: 'This key is the parameters passed as part of a command or application, etc.', + name: 'rsa.misc.param', + type: 'keyword', + }, + 'rsa.misc.change_attrib': { + category: 'rsa', + description: + 'This key is used to capture the name of the attribute that’s changing in a session', + name: 'rsa.misc.change_attrib', + type: 'keyword', + }, + 'rsa.misc.event_computer': { + category: 'rsa', + description: + 'This key is a windows only concept, where this key is used to capture fully qualified domain name in a windows log.', + name: 'rsa.misc.event_computer', + type: 'keyword', + }, + 'rsa.misc.reference_id1': { + category: 'rsa', + description: 'This key is for Linked ID to be used as an addition to "reference.id"', + name: 'rsa.misc.reference_id1', + type: 'keyword', + }, + 'rsa.misc.event_log': { + category: 'rsa', + description: 'This key captures the Name of the event log', + name: 'rsa.misc.event_log', + type: 'keyword', + }, + 'rsa.misc.OS': { + category: 'rsa', + description: 'This key captures the Name of the Operating System', + name: 'rsa.misc.OS', + type: 'keyword', + }, + 'rsa.misc.terminal': { + category: 'rsa', + description: 'This key captures the Terminal Names only', + name: 'rsa.misc.terminal', + type: 'keyword', + }, + 'rsa.misc.msgIdPart3': { + category: 'rsa', + name: 'rsa.misc.msgIdPart3', + type: 'keyword', + }, + 'rsa.misc.filter': { + category: 'rsa', + description: 'This key captures Filter used to reduce result set', + name: 'rsa.misc.filter', + type: 'keyword', + }, + 'rsa.misc.serial_number': { + category: 'rsa', + description: 'This key is the Serial number associated with a physical asset.', + name: 'rsa.misc.serial_number', + type: 'keyword', + }, + 'rsa.misc.checksum': { + category: 'rsa', + description: + 'This key is used to capture the checksum or hash of the entity such as a file or process. Checksum should be used over checksum.src or checksum.dst when it is unclear whether the entity is a source or target of an action.', + name: 'rsa.misc.checksum', + type: 'keyword', + }, + 'rsa.misc.event_user': { + category: 'rsa', + description: + 'This key is a windows only concept, where this key is used to capture combination of domain name and username in a windows log.', + name: 'rsa.misc.event_user', + type: 'keyword', + }, + 'rsa.misc.virusname': { + category: 'rsa', + description: 'This key captures the name of the virus', + name: 'rsa.misc.virusname', + type: 'keyword', + }, + 'rsa.misc.content_type': { + category: 'rsa', + description: 'This key is used to capture Content Type only.', + name: 'rsa.misc.content_type', + type: 'keyword', + }, + 'rsa.misc.group_id': { + category: 'rsa', + description: 'This key captures Group ID Number (related to the group name)', + name: 'rsa.misc.group_id', + type: 'keyword', + }, + 'rsa.misc.policy_id': { + category: 'rsa', + description: + 'This key is used to capture the Policy ID only, this should be a numeric value, use policy.name otherwise', + name: 'rsa.misc.policy_id', + type: 'keyword', + }, + 'rsa.misc.vsys': { + category: 'rsa', + description: 'This key captures Virtual System Name', + name: 'rsa.misc.vsys', + type: 'keyword', + }, + 'rsa.misc.connection_id': { + category: 'rsa', + description: 'This key captures the Connection ID', + name: 'rsa.misc.connection_id', + type: 'keyword', + }, + 'rsa.misc.reference_id2': { + category: 'rsa', + description: + 'This key is for the 2nd Linked ID. Can be either linked to "reference.id" or "reference.id1" value but should not be used unless the other two variables are in play.', + name: 'rsa.misc.reference_id2', + type: 'keyword', + }, + 'rsa.misc.sensor': { + category: 'rsa', + description: 'This key captures Name of the sensor. Typically used in IDS/IPS based devices', + name: 'rsa.misc.sensor', + type: 'keyword', + }, + 'rsa.misc.sig_id': { + category: 'rsa', + description: 'This key captures IDS/IPS Int Signature ID', + name: 'rsa.misc.sig_id', + type: 'long', + }, + 'rsa.misc.port_name': { + category: 'rsa', + description: + 'This key is used for Physical or logical port connection but does NOT include a network port. (Example: Printer port name).', + name: 'rsa.misc.port_name', + type: 'keyword', + }, + 'rsa.misc.rule_group': { + category: 'rsa', + description: 'This key captures the Rule group name', + name: 'rsa.misc.rule_group', + type: 'keyword', + }, + 'rsa.misc.risk_num': { + category: 'rsa', + description: 'This key captures a Numeric Risk value', + name: 'rsa.misc.risk_num', + type: 'double', + }, + 'rsa.misc.trigger_val': { + category: 'rsa', + description: 'This key captures the Value of the trigger or threshold condition.', + name: 'rsa.misc.trigger_val', + type: 'keyword', + }, + 'rsa.misc.log_session_id1': { + category: 'rsa', + description: + 'This key is used to capture a Linked (Related) Session ID from the session directly', + name: 'rsa.misc.log_session_id1', + type: 'keyword', + }, + 'rsa.misc.comp_version': { + category: 'rsa', + description: 'This key captures the Version level of a sub-component of a product.', + name: 'rsa.misc.comp_version', + type: 'keyword', + }, + 'rsa.misc.content_version': { + category: 'rsa', + description: 'This key captures Version level of a signature or database content.', + name: 'rsa.misc.content_version', + type: 'keyword', + }, + 'rsa.misc.hardware_id': { + category: 'rsa', + description: + 'This key is used to capture unique identifier for a device or system (NOT a Mac address)', + name: 'rsa.misc.hardware_id', + type: 'keyword', + }, + 'rsa.misc.risk': { + category: 'rsa', + description: 'This key captures the non-numeric risk value', + name: 'rsa.misc.risk', + type: 'keyword', + }, + 'rsa.misc.event_id': { + category: 'rsa', + name: 'rsa.misc.event_id', + type: 'keyword', + }, + 'rsa.misc.reason': { + category: 'rsa', + name: 'rsa.misc.reason', + type: 'keyword', + }, + 'rsa.misc.status': { + category: 'rsa', + name: 'rsa.misc.status', + type: 'keyword', + }, + 'rsa.misc.mail_id': { + category: 'rsa', + description: 'This key is used to capture the mailbox id/name', + name: 'rsa.misc.mail_id', + type: 'keyword', + }, + 'rsa.misc.rule_uid': { + category: 'rsa', + description: 'This key is the Unique Identifier for a rule.', + name: 'rsa.misc.rule_uid', + type: 'keyword', + }, + 'rsa.misc.trigger_desc': { + category: 'rsa', + description: 'This key captures the Description of the trigger or threshold condition.', + name: 'rsa.misc.trigger_desc', + type: 'keyword', + }, + 'rsa.misc.inout': { + category: 'rsa', + name: 'rsa.misc.inout', + type: 'keyword', + }, + 'rsa.misc.p_msgid': { + category: 'rsa', + name: 'rsa.misc.p_msgid', + type: 'keyword', + }, + 'rsa.misc.data_type': { + category: 'rsa', + name: 'rsa.misc.data_type', + type: 'keyword', + }, + 'rsa.misc.msgIdPart4': { + category: 'rsa', + name: 'rsa.misc.msgIdPart4', + type: 'keyword', + }, + 'rsa.misc.error': { + category: 'rsa', + description: 'This key captures All non successful Error codes or responses', + name: 'rsa.misc.error', + type: 'keyword', + }, + 'rsa.misc.index': { + category: 'rsa', + name: 'rsa.misc.index', + type: 'keyword', + }, + 'rsa.misc.listnum': { + category: 'rsa', + description: + 'This key is used to capture listname or listnumber, primarily for collecting access-list', + name: 'rsa.misc.listnum', + type: 'keyword', + }, + 'rsa.misc.ntype': { + category: 'rsa', + name: 'rsa.misc.ntype', + type: 'keyword', + }, + 'rsa.misc.observed_val': { + category: 'rsa', + description: + 'This key captures the Value observed (from the perspective of the device generating the log).', + name: 'rsa.misc.observed_val', + type: 'keyword', + }, + 'rsa.misc.policy_value': { + category: 'rsa', + description: + 'This key captures the contents of the policy. This contains details about the policy', + name: 'rsa.misc.policy_value', + type: 'keyword', + }, + 'rsa.misc.pool_name': { + category: 'rsa', + description: 'This key captures the name of a resource pool', + name: 'rsa.misc.pool_name', + type: 'keyword', + }, + 'rsa.misc.rule_template': { + category: 'rsa', + description: + 'A default set of parameters which are overlayed onto a rule (or rulename) which efffectively constitutes a template', + name: 'rsa.misc.rule_template', + type: 'keyword', + }, + 'rsa.misc.count': { + category: 'rsa', + name: 'rsa.misc.count', + type: 'keyword', + }, + 'rsa.misc.number': { + category: 'rsa', + name: 'rsa.misc.number', + type: 'keyword', + }, + 'rsa.misc.sigcat': { + category: 'rsa', + name: 'rsa.misc.sigcat', + type: 'keyword', + }, + 'rsa.misc.type': { + category: 'rsa', + name: 'rsa.misc.type', + type: 'keyword', + }, + 'rsa.misc.comments': { + category: 'rsa', + description: 'Comment information provided in the log message', + name: 'rsa.misc.comments', + type: 'keyword', + }, + 'rsa.misc.doc_number': { + category: 'rsa', + description: 'This key captures File Identification number', + name: 'rsa.misc.doc_number', + type: 'long', + }, + 'rsa.misc.expected_val': { + category: 'rsa', + description: + 'This key captures the Value expected (from the perspective of the device generating the log).', + name: 'rsa.misc.expected_val', + type: 'keyword', + }, + 'rsa.misc.job_num': { + category: 'rsa', + description: 'This key captures the Job Number', + name: 'rsa.misc.job_num', + type: 'keyword', + }, + 'rsa.misc.spi_dst': { + category: 'rsa', + description: 'Destination SPI Index', + name: 'rsa.misc.spi_dst', + type: 'keyword', + }, + 'rsa.misc.spi_src': { + category: 'rsa', + description: 'Source SPI Index', + name: 'rsa.misc.spi_src', + type: 'keyword', + }, + 'rsa.misc.code': { + category: 'rsa', + name: 'rsa.misc.code', + type: 'keyword', + }, + 'rsa.misc.agent_id': { + category: 'rsa', + description: 'This key is used to capture agent id', + name: 'rsa.misc.agent_id', + type: 'keyword', + }, + 'rsa.misc.message_body': { + category: 'rsa', + description: 'This key captures the The contents of the message body.', + name: 'rsa.misc.message_body', + type: 'keyword', + }, + 'rsa.misc.phone': { + category: 'rsa', + name: 'rsa.misc.phone', + type: 'keyword', + }, + 'rsa.misc.sig_id_str': { + category: 'rsa', + description: 'This key captures a string object of the sigid variable.', + name: 'rsa.misc.sig_id_str', + type: 'keyword', + }, + 'rsa.misc.cmd': { + category: 'rsa', + name: 'rsa.misc.cmd', + type: 'keyword', + }, + 'rsa.misc.misc': { + category: 'rsa', + name: 'rsa.misc.misc', + type: 'keyword', + }, + 'rsa.misc.name': { + category: 'rsa', + name: 'rsa.misc.name', + type: 'keyword', + }, + 'rsa.misc.cpu': { + category: 'rsa', + description: 'This key is the CPU time used in the execution of the event being recorded.', + name: 'rsa.misc.cpu', + type: 'long', + }, + 'rsa.misc.event_desc': { + category: 'rsa', + description: + 'This key is used to capture a description of an event available directly or inferred', + name: 'rsa.misc.event_desc', + type: 'keyword', + }, + 'rsa.misc.sig_id1': { + category: 'rsa', + description: 'This key captures IDS/IPS Int Signature ID. This must be linked to the sig.id', + name: 'rsa.misc.sig_id1', + type: 'long', + }, + 'rsa.misc.im_buddyid': { + category: 'rsa', + name: 'rsa.misc.im_buddyid', + type: 'keyword', + }, + 'rsa.misc.im_client': { + category: 'rsa', + name: 'rsa.misc.im_client', + type: 'keyword', + }, + 'rsa.misc.im_userid': { + category: 'rsa', + name: 'rsa.misc.im_userid', + type: 'keyword', + }, + 'rsa.misc.pid': { + category: 'rsa', + name: 'rsa.misc.pid', + type: 'keyword', + }, + 'rsa.misc.priority': { + category: 'rsa', + name: 'rsa.misc.priority', + type: 'keyword', + }, + 'rsa.misc.context_subject': { + category: 'rsa', + description: + 'This key is to be used in an audit context where the subject is the object being identified', + name: 'rsa.misc.context_subject', + type: 'keyword', + }, + 'rsa.misc.context_target': { + category: 'rsa', + name: 'rsa.misc.context_target', + type: 'keyword', + }, + 'rsa.misc.cve': { + category: 'rsa', + description: + 'This key captures CVE (Common Vulnerabilities and Exposures) - an identifier for known information security vulnerabilities.', + name: 'rsa.misc.cve', + type: 'keyword', + }, + 'rsa.misc.fcatnum': { + category: 'rsa', + description: 'This key captures Filter Category Number. Legacy Usage', + name: 'rsa.misc.fcatnum', + type: 'keyword', + }, + 'rsa.misc.library': { + category: 'rsa', + description: 'This key is used to capture library information in mainframe devices', + name: 'rsa.misc.library', + type: 'keyword', + }, + 'rsa.misc.parent_node': { + category: 'rsa', + description: 'This key captures the Parent Node Name. Must be related to node variable.', + name: 'rsa.misc.parent_node', + type: 'keyword', + }, + 'rsa.misc.risk_info': { + category: 'rsa', + description: 'Deprecated, use New Hunting Model (inv.*, ioc, boc, eoc, analysis.*)', + name: 'rsa.misc.risk_info', + type: 'keyword', + }, + 'rsa.misc.tcp_flags': { + category: 'rsa', + description: 'This key is captures the TCP flags set in any packet of session', + name: 'rsa.misc.tcp_flags', + type: 'long', + }, + 'rsa.misc.tos': { + category: 'rsa', + description: 'This key describes the type of service', + name: 'rsa.misc.tos', + type: 'long', + }, + 'rsa.misc.vm_target': { + category: 'rsa', + description: 'VMWare Target **VMWARE** only varaible.', + name: 'rsa.misc.vm_target', + type: 'keyword', + }, + 'rsa.misc.workspace': { + category: 'rsa', + description: 'This key captures Workspace Description', + name: 'rsa.misc.workspace', + type: 'keyword', + }, + 'rsa.misc.command': { + category: 'rsa', + name: 'rsa.misc.command', + type: 'keyword', + }, + 'rsa.misc.event_category': { + category: 'rsa', + name: 'rsa.misc.event_category', + type: 'keyword', + }, + 'rsa.misc.facilityname': { + category: 'rsa', + name: 'rsa.misc.facilityname', + type: 'keyword', + }, + 'rsa.misc.forensic_info': { + category: 'rsa', + name: 'rsa.misc.forensic_info', + type: 'keyword', + }, + 'rsa.misc.jobname': { + category: 'rsa', + name: 'rsa.misc.jobname', + type: 'keyword', + }, + 'rsa.misc.mode': { + category: 'rsa', + name: 'rsa.misc.mode', + type: 'keyword', + }, + 'rsa.misc.policy': { + category: 'rsa', + name: 'rsa.misc.policy', + type: 'keyword', + }, + 'rsa.misc.policy_waiver': { + category: 'rsa', + name: 'rsa.misc.policy_waiver', + type: 'keyword', + }, + 'rsa.misc.second': { + category: 'rsa', + name: 'rsa.misc.second', + type: 'keyword', + }, + 'rsa.misc.space1': { + category: 'rsa', + name: 'rsa.misc.space1', + type: 'keyword', + }, + 'rsa.misc.subcategory': { + category: 'rsa', + name: 'rsa.misc.subcategory', + type: 'keyword', + }, + 'rsa.misc.tbdstr2': { + category: 'rsa', + name: 'rsa.misc.tbdstr2', + type: 'keyword', + }, + 'rsa.misc.alert_id': { + category: 'rsa', + description: 'Deprecated, New Hunting Model (inv.*, ioc, boc, eoc, analysis.*)', + name: 'rsa.misc.alert_id', + type: 'keyword', + }, + 'rsa.misc.checksum_dst': { + category: 'rsa', + description: + 'This key is used to capture the checksum or hash of the the target entity such as a process or file.', + name: 'rsa.misc.checksum_dst', + type: 'keyword', + }, + 'rsa.misc.checksum_src': { + category: 'rsa', + description: + 'This key is used to capture the checksum or hash of the source entity such as a file or process.', + name: 'rsa.misc.checksum_src', + type: 'keyword', + }, + 'rsa.misc.fresult': { + category: 'rsa', + description: 'This key captures the Filter Result', + name: 'rsa.misc.fresult', + type: 'long', + }, + 'rsa.misc.payload_dst': { + category: 'rsa', + description: 'This key is used to capture destination payload', + name: 'rsa.misc.payload_dst', + type: 'keyword', + }, + 'rsa.misc.payload_src': { + category: 'rsa', + description: 'This key is used to capture source payload', + name: 'rsa.misc.payload_src', + type: 'keyword', + }, + 'rsa.misc.pool_id': { + category: 'rsa', + description: 'This key captures the identifier (typically numeric field) of a resource pool', + name: 'rsa.misc.pool_id', + type: 'keyword', + }, + 'rsa.misc.process_id_val': { + category: 'rsa', + description: 'This key is a failure key for Process ID when it is not an integer value', + name: 'rsa.misc.process_id_val', + type: 'keyword', + }, + 'rsa.misc.risk_num_comm': { + category: 'rsa', + description: 'This key captures Risk Number Community', + name: 'rsa.misc.risk_num_comm', + type: 'double', + }, + 'rsa.misc.risk_num_next': { + category: 'rsa', + description: 'This key captures Risk Number NextGen', + name: 'rsa.misc.risk_num_next', + type: 'double', + }, + 'rsa.misc.risk_num_sand': { + category: 'rsa', + description: 'This key captures Risk Number SandBox', + name: 'rsa.misc.risk_num_sand', + type: 'double', + }, + 'rsa.misc.risk_num_static': { + category: 'rsa', + description: 'This key captures Risk Number Static', + name: 'rsa.misc.risk_num_static', + type: 'double', + }, + 'rsa.misc.risk_suspicious': { + category: 'rsa', + description: 'Deprecated, use New Hunting Model (inv.*, ioc, boc, eoc, analysis.*)', + name: 'rsa.misc.risk_suspicious', + type: 'keyword', + }, + 'rsa.misc.risk_warning': { + category: 'rsa', + description: 'Deprecated, use New Hunting Model (inv.*, ioc, boc, eoc, analysis.*)', + name: 'rsa.misc.risk_warning', + type: 'keyword', + }, + 'rsa.misc.snmp_oid': { + category: 'rsa', + description: 'SNMP Object Identifier', + name: 'rsa.misc.snmp_oid', + type: 'keyword', + }, + 'rsa.misc.sql': { + category: 'rsa', + description: 'This key captures the SQL query', + name: 'rsa.misc.sql', + type: 'keyword', + }, + 'rsa.misc.vuln_ref': { + category: 'rsa', + description: 'This key captures the Vulnerability Reference details', + name: 'rsa.misc.vuln_ref', + type: 'keyword', + }, + 'rsa.misc.acl_id': { + category: 'rsa', + name: 'rsa.misc.acl_id', + type: 'keyword', + }, + 'rsa.misc.acl_op': { + category: 'rsa', + name: 'rsa.misc.acl_op', + type: 'keyword', + }, + 'rsa.misc.acl_pos': { + category: 'rsa', + name: 'rsa.misc.acl_pos', + type: 'keyword', + }, + 'rsa.misc.acl_table': { + category: 'rsa', + name: 'rsa.misc.acl_table', + type: 'keyword', + }, + 'rsa.misc.admin': { + category: 'rsa', + name: 'rsa.misc.admin', + type: 'keyword', + }, + 'rsa.misc.alarm_id': { + category: 'rsa', + name: 'rsa.misc.alarm_id', + type: 'keyword', + }, + 'rsa.misc.alarmname': { + category: 'rsa', + name: 'rsa.misc.alarmname', + type: 'keyword', + }, + 'rsa.misc.app_id': { + category: 'rsa', + name: 'rsa.misc.app_id', + type: 'keyword', + }, + 'rsa.misc.audit': { + category: 'rsa', + name: 'rsa.misc.audit', + type: 'keyword', + }, + 'rsa.misc.audit_object': { + category: 'rsa', + name: 'rsa.misc.audit_object', + type: 'keyword', + }, + 'rsa.misc.auditdata': { + category: 'rsa', + name: 'rsa.misc.auditdata', + type: 'keyword', + }, + 'rsa.misc.benchmark': { + category: 'rsa', + name: 'rsa.misc.benchmark', + type: 'keyword', + }, + 'rsa.misc.bypass': { + category: 'rsa', + name: 'rsa.misc.bypass', + type: 'keyword', + }, + 'rsa.misc.cache': { + category: 'rsa', + name: 'rsa.misc.cache', + type: 'keyword', + }, + 'rsa.misc.cache_hit': { + category: 'rsa', + name: 'rsa.misc.cache_hit', + type: 'keyword', + }, + 'rsa.misc.cefversion': { + category: 'rsa', + name: 'rsa.misc.cefversion', + type: 'keyword', + }, + 'rsa.misc.cfg_attr': { + category: 'rsa', + name: 'rsa.misc.cfg_attr', + type: 'keyword', + }, + 'rsa.misc.cfg_obj': { + category: 'rsa', + name: 'rsa.misc.cfg_obj', + type: 'keyword', + }, + 'rsa.misc.cfg_path': { + category: 'rsa', + name: 'rsa.misc.cfg_path', + type: 'keyword', + }, + 'rsa.misc.changes': { + category: 'rsa', + name: 'rsa.misc.changes', + type: 'keyword', + }, + 'rsa.misc.client_ip': { + category: 'rsa', + name: 'rsa.misc.client_ip', + type: 'keyword', + }, + 'rsa.misc.clustermembers': { + category: 'rsa', + name: 'rsa.misc.clustermembers', + type: 'keyword', + }, + 'rsa.misc.cn_acttimeout': { + category: 'rsa', + name: 'rsa.misc.cn_acttimeout', + type: 'keyword', + }, + 'rsa.misc.cn_asn_src': { + category: 'rsa', + name: 'rsa.misc.cn_asn_src', + type: 'keyword', + }, + 'rsa.misc.cn_bgpv4nxthop': { + category: 'rsa', + name: 'rsa.misc.cn_bgpv4nxthop', + type: 'keyword', + }, + 'rsa.misc.cn_ctr_dst_code': { + category: 'rsa', + name: 'rsa.misc.cn_ctr_dst_code', + type: 'keyword', + }, + 'rsa.misc.cn_dst_tos': { + category: 'rsa', + name: 'rsa.misc.cn_dst_tos', + type: 'keyword', + }, + 'rsa.misc.cn_dst_vlan': { + category: 'rsa', + name: 'rsa.misc.cn_dst_vlan', + type: 'keyword', + }, + 'rsa.misc.cn_engine_id': { + category: 'rsa', + name: 'rsa.misc.cn_engine_id', + type: 'keyword', + }, + 'rsa.misc.cn_engine_type': { + category: 'rsa', + name: 'rsa.misc.cn_engine_type', + type: 'keyword', + }, + 'rsa.misc.cn_f_switch': { + category: 'rsa', + name: 'rsa.misc.cn_f_switch', + type: 'keyword', + }, + 'rsa.misc.cn_flowsampid': { + category: 'rsa', + name: 'rsa.misc.cn_flowsampid', + type: 'keyword', + }, + 'rsa.misc.cn_flowsampintv': { + category: 'rsa', + name: 'rsa.misc.cn_flowsampintv', + type: 'keyword', + }, + 'rsa.misc.cn_flowsampmode': { + category: 'rsa', + name: 'rsa.misc.cn_flowsampmode', + type: 'keyword', + }, + 'rsa.misc.cn_inacttimeout': { + category: 'rsa', + name: 'rsa.misc.cn_inacttimeout', + type: 'keyword', + }, + 'rsa.misc.cn_inpermbyts': { + category: 'rsa', + name: 'rsa.misc.cn_inpermbyts', + type: 'keyword', + }, + 'rsa.misc.cn_inpermpckts': { + category: 'rsa', + name: 'rsa.misc.cn_inpermpckts', + type: 'keyword', + }, + 'rsa.misc.cn_invalid': { + category: 'rsa', + name: 'rsa.misc.cn_invalid', + type: 'keyword', + }, + 'rsa.misc.cn_ip_proto_ver': { + category: 'rsa', + name: 'rsa.misc.cn_ip_proto_ver', + type: 'keyword', + }, + 'rsa.misc.cn_ipv4_ident': { + category: 'rsa', + name: 'rsa.misc.cn_ipv4_ident', + type: 'keyword', + }, + 'rsa.misc.cn_l_switch': { + category: 'rsa', + name: 'rsa.misc.cn_l_switch', + type: 'keyword', + }, + 'rsa.misc.cn_log_did': { + category: 'rsa', + name: 'rsa.misc.cn_log_did', + type: 'keyword', + }, + 'rsa.misc.cn_log_rid': { + category: 'rsa', + name: 'rsa.misc.cn_log_rid', + type: 'keyword', + }, + 'rsa.misc.cn_max_ttl': { + category: 'rsa', + name: 'rsa.misc.cn_max_ttl', + type: 'keyword', + }, + 'rsa.misc.cn_maxpcktlen': { + category: 'rsa', + name: 'rsa.misc.cn_maxpcktlen', + type: 'keyword', + }, + 'rsa.misc.cn_min_ttl': { + category: 'rsa', + name: 'rsa.misc.cn_min_ttl', + type: 'keyword', + }, + 'rsa.misc.cn_minpcktlen': { + category: 'rsa', + name: 'rsa.misc.cn_minpcktlen', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_1': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_1', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_10': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_10', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_2': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_2', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_3': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_3', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_4': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_4', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_5': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_5', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_6': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_6', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_7': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_7', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_8': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_8', + type: 'keyword', + }, + 'rsa.misc.cn_mpls_lbl_9': { + category: 'rsa', + name: 'rsa.misc.cn_mpls_lbl_9', + type: 'keyword', + }, + 'rsa.misc.cn_mplstoplabel': { + category: 'rsa', + name: 'rsa.misc.cn_mplstoplabel', + type: 'keyword', + }, + 'rsa.misc.cn_mplstoplabip': { + category: 'rsa', + name: 'rsa.misc.cn_mplstoplabip', + type: 'keyword', + }, + 'rsa.misc.cn_mul_dst_byt': { + category: 'rsa', + name: 'rsa.misc.cn_mul_dst_byt', + type: 'keyword', + }, + 'rsa.misc.cn_mul_dst_pks': { + category: 'rsa', + name: 'rsa.misc.cn_mul_dst_pks', + type: 'keyword', + }, + 'rsa.misc.cn_muligmptype': { + category: 'rsa', + name: 'rsa.misc.cn_muligmptype', + type: 'keyword', + }, + 'rsa.misc.cn_sampalgo': { + category: 'rsa', + name: 'rsa.misc.cn_sampalgo', + type: 'keyword', + }, + 'rsa.misc.cn_sampint': { + category: 'rsa', + name: 'rsa.misc.cn_sampint', + type: 'keyword', + }, + 'rsa.misc.cn_seqctr': { + category: 'rsa', + name: 'rsa.misc.cn_seqctr', + type: 'keyword', + }, + 'rsa.misc.cn_spackets': { + category: 'rsa', + name: 'rsa.misc.cn_spackets', + type: 'keyword', + }, + 'rsa.misc.cn_src_tos': { + category: 'rsa', + name: 'rsa.misc.cn_src_tos', + type: 'keyword', + }, + 'rsa.misc.cn_src_vlan': { + category: 'rsa', + name: 'rsa.misc.cn_src_vlan', + type: 'keyword', + }, + 'rsa.misc.cn_sysuptime': { + category: 'rsa', + name: 'rsa.misc.cn_sysuptime', + type: 'keyword', + }, + 'rsa.misc.cn_template_id': { + category: 'rsa', + name: 'rsa.misc.cn_template_id', + type: 'keyword', + }, + 'rsa.misc.cn_totbytsexp': { + category: 'rsa', + name: 'rsa.misc.cn_totbytsexp', + type: 'keyword', + }, + 'rsa.misc.cn_totflowexp': { + category: 'rsa', + name: 'rsa.misc.cn_totflowexp', + type: 'keyword', + }, + 'rsa.misc.cn_totpcktsexp': { + category: 'rsa', + name: 'rsa.misc.cn_totpcktsexp', + type: 'keyword', + }, + 'rsa.misc.cn_unixnanosecs': { + category: 'rsa', + name: 'rsa.misc.cn_unixnanosecs', + type: 'keyword', + }, + 'rsa.misc.cn_v6flowlabel': { + category: 'rsa', + name: 'rsa.misc.cn_v6flowlabel', + type: 'keyword', + }, + 'rsa.misc.cn_v6optheaders': { + category: 'rsa', + name: 'rsa.misc.cn_v6optheaders', + type: 'keyword', + }, + 'rsa.misc.comp_class': { + category: 'rsa', + name: 'rsa.misc.comp_class', + type: 'keyword', + }, + 'rsa.misc.comp_name': { + category: 'rsa', + name: 'rsa.misc.comp_name', + type: 'keyword', + }, + 'rsa.misc.comp_rbytes': { + category: 'rsa', + name: 'rsa.misc.comp_rbytes', + type: 'keyword', + }, + 'rsa.misc.comp_sbytes': { + category: 'rsa', + name: 'rsa.misc.comp_sbytes', + type: 'keyword', + }, + 'rsa.misc.cpu_data': { + category: 'rsa', + name: 'rsa.misc.cpu_data', + type: 'keyword', + }, + 'rsa.misc.criticality': { + category: 'rsa', + name: 'rsa.misc.criticality', + type: 'keyword', + }, + 'rsa.misc.cs_agency_dst': { + category: 'rsa', + name: 'rsa.misc.cs_agency_dst', + type: 'keyword', + }, + 'rsa.misc.cs_analyzedby': { + category: 'rsa', + name: 'rsa.misc.cs_analyzedby', + type: 'keyword', + }, + 'rsa.misc.cs_av_other': { + category: 'rsa', + name: 'rsa.misc.cs_av_other', + type: 'keyword', + }, + 'rsa.misc.cs_av_primary': { + category: 'rsa', + name: 'rsa.misc.cs_av_primary', + type: 'keyword', + }, + 'rsa.misc.cs_av_secondary': { + category: 'rsa', + name: 'rsa.misc.cs_av_secondary', + type: 'keyword', + }, + 'rsa.misc.cs_bgpv6nxthop': { + category: 'rsa', + name: 'rsa.misc.cs_bgpv6nxthop', + type: 'keyword', + }, + 'rsa.misc.cs_bit9status': { + category: 'rsa', + name: 'rsa.misc.cs_bit9status', + type: 'keyword', + }, + 'rsa.misc.cs_context': { + category: 'rsa', + name: 'rsa.misc.cs_context', + type: 'keyword', + }, + 'rsa.misc.cs_control': { + category: 'rsa', + name: 'rsa.misc.cs_control', + type: 'keyword', + }, + 'rsa.misc.cs_data': { + category: 'rsa', + name: 'rsa.misc.cs_data', + type: 'keyword', + }, + 'rsa.misc.cs_datecret': { + category: 'rsa', + name: 'rsa.misc.cs_datecret', + type: 'keyword', + }, + 'rsa.misc.cs_dst_tld': { + category: 'rsa', + name: 'rsa.misc.cs_dst_tld', + type: 'keyword', + }, + 'rsa.misc.cs_eth_dst_ven': { + category: 'rsa', + name: 'rsa.misc.cs_eth_dst_ven', + type: 'keyword', + }, + 'rsa.misc.cs_eth_src_ven': { + category: 'rsa', + name: 'rsa.misc.cs_eth_src_ven', + type: 'keyword', + }, + 'rsa.misc.cs_event_uuid': { + category: 'rsa', + name: 'rsa.misc.cs_event_uuid', + type: 'keyword', + }, + 'rsa.misc.cs_filetype': { + category: 'rsa', + name: 'rsa.misc.cs_filetype', + type: 'keyword', + }, + 'rsa.misc.cs_fld': { + category: 'rsa', + name: 'rsa.misc.cs_fld', + type: 'keyword', + }, + 'rsa.misc.cs_if_desc': { + category: 'rsa', + name: 'rsa.misc.cs_if_desc', + type: 'keyword', + }, + 'rsa.misc.cs_if_name': { + category: 'rsa', + name: 'rsa.misc.cs_if_name', + type: 'keyword', + }, + 'rsa.misc.cs_ip_next_hop': { + category: 'rsa', + name: 'rsa.misc.cs_ip_next_hop', + type: 'keyword', + }, + 'rsa.misc.cs_ipv4dstpre': { + category: 'rsa', + name: 'rsa.misc.cs_ipv4dstpre', + type: 'keyword', + }, + 'rsa.misc.cs_ipv4srcpre': { + category: 'rsa', + name: 'rsa.misc.cs_ipv4srcpre', + type: 'keyword', + }, + 'rsa.misc.cs_lifetime': { + category: 'rsa', + name: 'rsa.misc.cs_lifetime', + type: 'keyword', + }, + 'rsa.misc.cs_log_medium': { + category: 'rsa', + name: 'rsa.misc.cs_log_medium', + type: 'keyword', + }, + 'rsa.misc.cs_loginname': { + category: 'rsa', + name: 'rsa.misc.cs_loginname', + type: 'keyword', + }, + 'rsa.misc.cs_modulescore': { + category: 'rsa', + name: 'rsa.misc.cs_modulescore', + type: 'keyword', + }, + 'rsa.misc.cs_modulesign': { + category: 'rsa', + name: 'rsa.misc.cs_modulesign', + type: 'keyword', + }, + 'rsa.misc.cs_opswatresult': { + category: 'rsa', + name: 'rsa.misc.cs_opswatresult', + type: 'keyword', + }, + 'rsa.misc.cs_payload': { + category: 'rsa', + name: 'rsa.misc.cs_payload', + type: 'keyword', + }, + 'rsa.misc.cs_registrant': { + category: 'rsa', + name: 'rsa.misc.cs_registrant', + type: 'keyword', + }, + 'rsa.misc.cs_registrar': { + category: 'rsa', + name: 'rsa.misc.cs_registrar', + type: 'keyword', + }, + 'rsa.misc.cs_represult': { + category: 'rsa', + name: 'rsa.misc.cs_represult', + type: 'keyword', + }, + 'rsa.misc.cs_rpayload': { + category: 'rsa', + name: 'rsa.misc.cs_rpayload', + type: 'keyword', + }, + 'rsa.misc.cs_sampler_name': { + category: 'rsa', + name: 'rsa.misc.cs_sampler_name', + type: 'keyword', + }, + 'rsa.misc.cs_sourcemodule': { + category: 'rsa', + name: 'rsa.misc.cs_sourcemodule', + type: 'keyword', + }, + 'rsa.misc.cs_streams': { + category: 'rsa', + name: 'rsa.misc.cs_streams', + type: 'keyword', + }, + 'rsa.misc.cs_targetmodule': { + category: 'rsa', + name: 'rsa.misc.cs_targetmodule', + type: 'keyword', + }, + 'rsa.misc.cs_v6nxthop': { + category: 'rsa', + name: 'rsa.misc.cs_v6nxthop', + type: 'keyword', + }, + 'rsa.misc.cs_whois_server': { + category: 'rsa', + name: 'rsa.misc.cs_whois_server', + type: 'keyword', + }, + 'rsa.misc.cs_yararesult': { + category: 'rsa', + name: 'rsa.misc.cs_yararesult', + type: 'keyword', + }, + 'rsa.misc.description': { + category: 'rsa', + name: 'rsa.misc.description', + type: 'keyword', + }, + 'rsa.misc.devvendor': { + category: 'rsa', + name: 'rsa.misc.devvendor', + type: 'keyword', + }, + 'rsa.misc.distance': { + category: 'rsa', + name: 'rsa.misc.distance', + type: 'keyword', + }, + 'rsa.misc.dstburb': { + category: 'rsa', + name: 'rsa.misc.dstburb', + type: 'keyword', + }, + 'rsa.misc.edomain': { + category: 'rsa', + name: 'rsa.misc.edomain', + type: 'keyword', + }, + 'rsa.misc.edomaub': { + category: 'rsa', + name: 'rsa.misc.edomaub', + type: 'keyword', + }, + 'rsa.misc.euid': { + category: 'rsa', + name: 'rsa.misc.euid', + type: 'keyword', + }, + 'rsa.misc.facility': { + category: 'rsa', + name: 'rsa.misc.facility', + type: 'keyword', + }, + 'rsa.misc.finterface': { + category: 'rsa', + name: 'rsa.misc.finterface', + type: 'keyword', + }, + 'rsa.misc.flags': { + category: 'rsa', + name: 'rsa.misc.flags', + type: 'keyword', + }, + 'rsa.misc.gaddr': { + category: 'rsa', + name: 'rsa.misc.gaddr', + type: 'keyword', + }, + 'rsa.misc.id3': { + category: 'rsa', + name: 'rsa.misc.id3', + type: 'keyword', + }, + 'rsa.misc.im_buddyname': { + category: 'rsa', + name: 'rsa.misc.im_buddyname', + type: 'keyword', + }, + 'rsa.misc.im_croomid': { + category: 'rsa', + name: 'rsa.misc.im_croomid', + type: 'keyword', + }, + 'rsa.misc.im_croomtype': { + category: 'rsa', + name: 'rsa.misc.im_croomtype', + type: 'keyword', + }, + 'rsa.misc.im_members': { + category: 'rsa', + name: 'rsa.misc.im_members', + type: 'keyword', + }, + 'rsa.misc.im_username': { + category: 'rsa', + name: 'rsa.misc.im_username', + type: 'keyword', + }, + 'rsa.misc.ipkt': { + category: 'rsa', + name: 'rsa.misc.ipkt', + type: 'keyword', + }, + 'rsa.misc.ipscat': { + category: 'rsa', + name: 'rsa.misc.ipscat', + type: 'keyword', + }, + 'rsa.misc.ipspri': { + category: 'rsa', + name: 'rsa.misc.ipspri', + type: 'keyword', + }, + 'rsa.misc.latitude': { + category: 'rsa', + name: 'rsa.misc.latitude', + type: 'keyword', + }, + 'rsa.misc.linenum': { + category: 'rsa', + name: 'rsa.misc.linenum', + type: 'keyword', + }, + 'rsa.misc.list_name': { + category: 'rsa', + name: 'rsa.misc.list_name', + type: 'keyword', + }, + 'rsa.misc.load_data': { + category: 'rsa', + name: 'rsa.misc.load_data', + type: 'keyword', + }, + 'rsa.misc.location_floor': { + category: 'rsa', + name: 'rsa.misc.location_floor', + type: 'keyword', + }, + 'rsa.misc.location_mark': { + category: 'rsa', + name: 'rsa.misc.location_mark', + type: 'keyword', + }, + 'rsa.misc.log_id': { + category: 'rsa', + name: 'rsa.misc.log_id', + type: 'keyword', + }, + 'rsa.misc.log_type': { + category: 'rsa', + name: 'rsa.misc.log_type', + type: 'keyword', + }, + 'rsa.misc.logid': { + category: 'rsa', + name: 'rsa.misc.logid', + type: 'keyword', + }, + 'rsa.misc.logip': { + category: 'rsa', + name: 'rsa.misc.logip', + type: 'keyword', + }, + 'rsa.misc.logname': { + category: 'rsa', + name: 'rsa.misc.logname', + type: 'keyword', + }, + 'rsa.misc.longitude': { + category: 'rsa', + name: 'rsa.misc.longitude', + type: 'keyword', + }, + 'rsa.misc.lport': { + category: 'rsa', + name: 'rsa.misc.lport', + type: 'keyword', + }, + 'rsa.misc.mbug_data': { + category: 'rsa', + name: 'rsa.misc.mbug_data', + type: 'keyword', + }, + 'rsa.misc.misc_name': { + category: 'rsa', + name: 'rsa.misc.misc_name', + type: 'keyword', + }, + 'rsa.misc.msg_type': { + category: 'rsa', + name: 'rsa.misc.msg_type', + type: 'keyword', + }, + 'rsa.misc.msgid': { + category: 'rsa', + name: 'rsa.misc.msgid', + type: 'keyword', + }, + 'rsa.misc.netsessid': { + category: 'rsa', + name: 'rsa.misc.netsessid', + type: 'keyword', + }, + 'rsa.misc.num': { + category: 'rsa', + name: 'rsa.misc.num', + type: 'keyword', + }, + 'rsa.misc.number1': { + category: 'rsa', + name: 'rsa.misc.number1', + type: 'keyword', + }, + 'rsa.misc.number2': { + category: 'rsa', + name: 'rsa.misc.number2', + type: 'keyword', + }, + 'rsa.misc.nwwn': { + category: 'rsa', + name: 'rsa.misc.nwwn', + type: 'keyword', + }, + 'rsa.misc.object': { + category: 'rsa', + name: 'rsa.misc.object', + type: 'keyword', + }, + 'rsa.misc.operation': { + category: 'rsa', + name: 'rsa.misc.operation', + type: 'keyword', + }, + 'rsa.misc.opkt': { + category: 'rsa', + name: 'rsa.misc.opkt', + type: 'keyword', + }, + 'rsa.misc.orig_from': { + category: 'rsa', + name: 'rsa.misc.orig_from', + type: 'keyword', + }, + 'rsa.misc.owner_id': { + category: 'rsa', + name: 'rsa.misc.owner_id', + type: 'keyword', + }, + 'rsa.misc.p_action': { + category: 'rsa', + name: 'rsa.misc.p_action', + type: 'keyword', + }, + 'rsa.misc.p_filter': { + category: 'rsa', + name: 'rsa.misc.p_filter', + type: 'keyword', + }, + 'rsa.misc.p_group_object': { + category: 'rsa', + name: 'rsa.misc.p_group_object', + type: 'keyword', + }, + 'rsa.misc.p_id': { + category: 'rsa', + name: 'rsa.misc.p_id', + type: 'keyword', + }, + 'rsa.misc.p_msgid1': { + category: 'rsa', + name: 'rsa.misc.p_msgid1', + type: 'keyword', + }, + 'rsa.misc.p_msgid2': { + category: 'rsa', + name: 'rsa.misc.p_msgid2', + type: 'keyword', + }, + 'rsa.misc.p_result1': { + category: 'rsa', + name: 'rsa.misc.p_result1', + type: 'keyword', + }, + 'rsa.misc.password_chg': { + category: 'rsa', + name: 'rsa.misc.password_chg', + type: 'keyword', + }, + 'rsa.misc.password_expire': { + category: 'rsa', + name: 'rsa.misc.password_expire', + type: 'keyword', + }, + 'rsa.misc.permgranted': { + category: 'rsa', + name: 'rsa.misc.permgranted', + type: 'keyword', + }, + 'rsa.misc.permwanted': { + category: 'rsa', + name: 'rsa.misc.permwanted', + type: 'keyword', + }, + 'rsa.misc.pgid': { + category: 'rsa', + name: 'rsa.misc.pgid', + type: 'keyword', + }, + 'rsa.misc.policyUUID': { + category: 'rsa', + name: 'rsa.misc.policyUUID', + type: 'keyword', + }, + 'rsa.misc.prog_asp_num': { + category: 'rsa', + name: 'rsa.misc.prog_asp_num', + type: 'keyword', + }, + 'rsa.misc.program': { + category: 'rsa', + name: 'rsa.misc.program', + type: 'keyword', + }, + 'rsa.misc.real_data': { + category: 'rsa', + name: 'rsa.misc.real_data', + type: 'keyword', + }, + 'rsa.misc.rec_asp_device': { + category: 'rsa', + name: 'rsa.misc.rec_asp_device', + type: 'keyword', + }, + 'rsa.misc.rec_asp_num': { + category: 'rsa', + name: 'rsa.misc.rec_asp_num', + type: 'keyword', + }, + 'rsa.misc.rec_library': { + category: 'rsa', + name: 'rsa.misc.rec_library', + type: 'keyword', + }, + 'rsa.misc.recordnum': { + category: 'rsa', + name: 'rsa.misc.recordnum', + type: 'keyword', + }, + 'rsa.misc.ruid': { + category: 'rsa', + name: 'rsa.misc.ruid', + type: 'keyword', + }, + 'rsa.misc.sburb': { + category: 'rsa', + name: 'rsa.misc.sburb', + type: 'keyword', + }, + 'rsa.misc.sdomain_fld': { + category: 'rsa', + name: 'rsa.misc.sdomain_fld', + type: 'keyword', + }, + 'rsa.misc.sec': { + category: 'rsa', + name: 'rsa.misc.sec', + type: 'keyword', + }, + 'rsa.misc.sensorname': { + category: 'rsa', + name: 'rsa.misc.sensorname', + type: 'keyword', + }, + 'rsa.misc.seqnum': { + category: 'rsa', + name: 'rsa.misc.seqnum', + type: 'keyword', + }, + 'rsa.misc.session': { + category: 'rsa', + name: 'rsa.misc.session', + type: 'keyword', + }, + 'rsa.misc.sessiontype': { + category: 'rsa', + name: 'rsa.misc.sessiontype', + type: 'keyword', + }, + 'rsa.misc.sigUUID': { + category: 'rsa', + name: 'rsa.misc.sigUUID', + type: 'keyword', + }, + 'rsa.misc.spi': { + category: 'rsa', + name: 'rsa.misc.spi', + type: 'keyword', + }, + 'rsa.misc.srcburb': { + category: 'rsa', + name: 'rsa.misc.srcburb', + type: 'keyword', + }, + 'rsa.misc.srcdom': { + category: 'rsa', + name: 'rsa.misc.srcdom', + type: 'keyword', + }, + 'rsa.misc.srcservice': { + category: 'rsa', + name: 'rsa.misc.srcservice', + type: 'keyword', + }, + 'rsa.misc.state': { + category: 'rsa', + name: 'rsa.misc.state', + type: 'keyword', + }, + 'rsa.misc.status1': { + category: 'rsa', + name: 'rsa.misc.status1', + type: 'keyword', + }, + 'rsa.misc.svcno': { + category: 'rsa', + name: 'rsa.misc.svcno', + type: 'keyword', + }, + 'rsa.misc.system': { + category: 'rsa', + name: 'rsa.misc.system', + type: 'keyword', + }, + 'rsa.misc.tbdstr1': { + category: 'rsa', + name: 'rsa.misc.tbdstr1', + type: 'keyword', + }, + 'rsa.misc.tgtdom': { + category: 'rsa', + name: 'rsa.misc.tgtdom', + type: 'keyword', + }, + 'rsa.misc.tgtdomain': { + category: 'rsa', + name: 'rsa.misc.tgtdomain', + type: 'keyword', + }, + 'rsa.misc.threshold': { + category: 'rsa', + name: 'rsa.misc.threshold', + type: 'keyword', + }, + 'rsa.misc.type1': { + category: 'rsa', + name: 'rsa.misc.type1', + type: 'keyword', + }, + 'rsa.misc.udb_class': { + category: 'rsa', + name: 'rsa.misc.udb_class', + type: 'keyword', + }, + 'rsa.misc.url_fld': { + category: 'rsa', + name: 'rsa.misc.url_fld', + type: 'keyword', + }, + 'rsa.misc.user_div': { + category: 'rsa', + name: 'rsa.misc.user_div', + type: 'keyword', + }, + 'rsa.misc.userid': { + category: 'rsa', + name: 'rsa.misc.userid', + type: 'keyword', + }, + 'rsa.misc.username_fld': { + category: 'rsa', + name: 'rsa.misc.username_fld', + type: 'keyword', + }, + 'rsa.misc.utcstamp': { + category: 'rsa', + name: 'rsa.misc.utcstamp', + type: 'keyword', + }, + 'rsa.misc.v_instafname': { + category: 'rsa', + name: 'rsa.misc.v_instafname', + type: 'keyword', + }, + 'rsa.misc.virt_data': { + category: 'rsa', + name: 'rsa.misc.virt_data', + type: 'keyword', + }, + 'rsa.misc.vpnid': { + category: 'rsa', + name: 'rsa.misc.vpnid', + type: 'keyword', + }, + 'rsa.misc.autorun_type': { + category: 'rsa', + description: 'This is used to capture Auto Run type', + name: 'rsa.misc.autorun_type', + type: 'keyword', + }, + 'rsa.misc.cc_number': { + category: 'rsa', + description: 'Valid Credit Card Numbers only', + name: 'rsa.misc.cc_number', + type: 'long', + }, + 'rsa.misc.content': { + category: 'rsa', + description: 'This key captures the content type from protocol headers', + name: 'rsa.misc.content', + type: 'keyword', + }, + 'rsa.misc.ein_number': { + category: 'rsa', + description: 'Employee Identification Numbers only', + name: 'rsa.misc.ein_number', + type: 'long', + }, + 'rsa.misc.found': { + category: 'rsa', + description: 'This is used to capture the results of regex match', + name: 'rsa.misc.found', + type: 'keyword', + }, + 'rsa.misc.language': { + category: 'rsa', + description: 'This is used to capture list of languages the client support and what it prefers', + name: 'rsa.misc.language', + type: 'keyword', + }, + 'rsa.misc.lifetime': { + category: 'rsa', + description: 'This key is used to capture the session lifetime in seconds.', + name: 'rsa.misc.lifetime', + type: 'long', + }, + 'rsa.misc.link': { + category: 'rsa', + description: + 'This key is used to link the sessions together. This key should never be used to parse Meta data from a session (Logs/Packets) Directly, this is a Reserved key in NetWitness', + name: 'rsa.misc.link', + type: 'keyword', + }, + 'rsa.misc.match': { + category: 'rsa', + description: 'This key is for regex match name from search.ini', + name: 'rsa.misc.match', + type: 'keyword', + }, + 'rsa.misc.param_dst': { + category: 'rsa', + description: 'This key captures the command line/launch argument of the target process or file', + name: 'rsa.misc.param_dst', + type: 'keyword', + }, + 'rsa.misc.param_src': { + category: 'rsa', + description: 'This key captures source parameter', + name: 'rsa.misc.param_src', + type: 'keyword', + }, + 'rsa.misc.search_text': { + category: 'rsa', + description: 'This key captures the Search Text used', + name: 'rsa.misc.search_text', + type: 'keyword', + }, + 'rsa.misc.sig_name': { + category: 'rsa', + description: 'This key is used to capture the Signature Name only.', + name: 'rsa.misc.sig_name', + type: 'keyword', + }, + 'rsa.misc.snmp_value': { + category: 'rsa', + description: 'SNMP set request value', + name: 'rsa.misc.snmp_value', + type: 'keyword', + }, + 'rsa.misc.streams': { + category: 'rsa', + description: 'This key captures number of streams in session', + name: 'rsa.misc.streams', + type: 'long', + }, + 'rsa.db.index': { + category: 'rsa', + description: 'This key captures IndexID of the index.', + name: 'rsa.db.index', + type: 'keyword', + }, + 'rsa.db.instance': { + category: 'rsa', + description: 'This key is used to capture the database server instance name', + name: 'rsa.db.instance', + type: 'keyword', + }, + 'rsa.db.database': { + category: 'rsa', + description: + 'This key is used to capture the name of a database or an instance as seen in a session', + name: 'rsa.db.database', + type: 'keyword', + }, + 'rsa.db.transact_id': { + category: 'rsa', + description: 'This key captures the SQL transantion ID of the current session', + name: 'rsa.db.transact_id', + type: 'keyword', + }, + 'rsa.db.permissions': { + category: 'rsa', + description: 'This key captures permission or privilege level assigned to a resource.', + name: 'rsa.db.permissions', + type: 'keyword', + }, + 'rsa.db.table_name': { + category: 'rsa', + description: 'This key is used to capture the table name', + name: 'rsa.db.table_name', + type: 'keyword', + }, + 'rsa.db.db_id': { + category: 'rsa', + description: 'This key is used to capture the unique identifier for a database', + name: 'rsa.db.db_id', + type: 'keyword', + }, + 'rsa.db.db_pid': { + category: 'rsa', + description: 'This key captures the process id of a connection with database server', + name: 'rsa.db.db_pid', + type: 'long', + }, + 'rsa.db.lread': { + category: 'rsa', + description: 'This key is used for the number of logical reads', + name: 'rsa.db.lread', + type: 'long', + }, + 'rsa.db.lwrite': { + category: 'rsa', + description: 'This key is used for the number of logical writes', + name: 'rsa.db.lwrite', + type: 'long', + }, + 'rsa.db.pread': { + category: 'rsa', + description: 'This key is used for the number of physical writes', + name: 'rsa.db.pread', + type: 'long', + }, + 'rsa.network.alias_host': { + category: 'rsa', + description: + 'This key should be used when the source or destination context of a hostname is not clear.Also it captures the Device Hostname. Any Hostname that isnt ad.computer.', + name: 'rsa.network.alias_host', + type: 'keyword', + }, + 'rsa.network.domain': { + category: 'rsa', + name: 'rsa.network.domain', + type: 'keyword', + }, + 'rsa.network.host_dst': { + category: 'rsa', + description: 'This key should only be used when it’s a Destination Hostname', + name: 'rsa.network.host_dst', + type: 'keyword', + }, + 'rsa.network.network_service': { + category: 'rsa', + description: 'This is used to capture layer 7 protocols/service names', + name: 'rsa.network.network_service', + type: 'keyword', + }, + 'rsa.network.interface': { + category: 'rsa', + description: + 'This key should be used when the source or destination context of an interface is not clear', + name: 'rsa.network.interface', + type: 'keyword', + }, + 'rsa.network.network_port': { + category: 'rsa', + description: + 'Deprecated, use port. NOTE: There is a type discrepancy as currently used, TM: Int32, INDEX: UInt64 (why neither chose the correct UInt16?!)', + name: 'rsa.network.network_port', + type: 'long', + }, + 'rsa.network.eth_host': { + category: 'rsa', + description: 'Deprecated, use alias.mac', + name: 'rsa.network.eth_host', + type: 'keyword', + }, + 'rsa.network.sinterface': { + category: 'rsa', + description: 'This key should only be used when it’s a Source Interface', + name: 'rsa.network.sinterface', + type: 'keyword', + }, + 'rsa.network.dinterface': { + category: 'rsa', + description: 'This key should only be used when it’s a Destination Interface', + name: 'rsa.network.dinterface', + type: 'keyword', + }, + 'rsa.network.vlan': { + category: 'rsa', + description: 'This key should only be used to capture the ID of the Virtual LAN', + name: 'rsa.network.vlan', + type: 'long', + }, + 'rsa.network.zone_src': { + category: 'rsa', + description: 'This key should only be used when it’s a Source Zone.', + name: 'rsa.network.zone_src', + type: 'keyword', + }, + 'rsa.network.zone': { + category: 'rsa', + description: + 'This key should be used when the source or destination context of a Zone is not clear', + name: 'rsa.network.zone', + type: 'keyword', + }, + 'rsa.network.zone_dst': { + category: 'rsa', + description: 'This key should only be used when it’s a Destination Zone.', + name: 'rsa.network.zone_dst', + type: 'keyword', + }, + 'rsa.network.gateway': { + category: 'rsa', + description: 'This key is used to capture the IP Address of the gateway', + name: 'rsa.network.gateway', + type: 'keyword', + }, + 'rsa.network.icmp_type': { + category: 'rsa', + description: 'This key is used to capture the ICMP type only', + name: 'rsa.network.icmp_type', + type: 'long', + }, + 'rsa.network.mask': { + category: 'rsa', + description: 'This key is used to capture the device network IPmask.', + name: 'rsa.network.mask', + type: 'keyword', + }, + 'rsa.network.icmp_code': { + category: 'rsa', + description: 'This key is used to capture the ICMP code only', + name: 'rsa.network.icmp_code', + type: 'long', + }, + 'rsa.network.protocol_detail': { + category: 'rsa', + description: 'This key should be used to capture additional protocol information', + name: 'rsa.network.protocol_detail', + type: 'keyword', + }, + 'rsa.network.dmask': { + category: 'rsa', + description: 'This key is used for Destionation Device network mask', + name: 'rsa.network.dmask', + type: 'keyword', + }, + 'rsa.network.port': { + category: 'rsa', + description: + 'This key should only be used to capture a Network Port when the directionality is not clear', + name: 'rsa.network.port', + type: 'long', + }, + 'rsa.network.smask': { + category: 'rsa', + description: 'This key is used for capturing source Network Mask', + name: 'rsa.network.smask', + type: 'keyword', + }, + 'rsa.network.netname': { + category: 'rsa', + description: + 'This key is used to capture the network name associated with an IP range. This is configured by the end user.', + name: 'rsa.network.netname', + type: 'keyword', + }, + 'rsa.network.paddr': { + category: 'rsa', + description: 'Deprecated', + name: 'rsa.network.paddr', + type: 'ip', + }, + 'rsa.network.faddr': { + category: 'rsa', + name: 'rsa.network.faddr', + type: 'keyword', + }, + 'rsa.network.lhost': { + category: 'rsa', + name: 'rsa.network.lhost', + type: 'keyword', + }, + 'rsa.network.origin': { + category: 'rsa', + name: 'rsa.network.origin', + type: 'keyword', + }, + 'rsa.network.remote_domain_id': { + category: 'rsa', + name: 'rsa.network.remote_domain_id', + type: 'keyword', + }, + 'rsa.network.addr': { + category: 'rsa', + name: 'rsa.network.addr', + type: 'keyword', + }, + 'rsa.network.dns_a_record': { + category: 'rsa', + name: 'rsa.network.dns_a_record', + type: 'keyword', + }, + 'rsa.network.dns_ptr_record': { + category: 'rsa', + name: 'rsa.network.dns_ptr_record', + type: 'keyword', + }, + 'rsa.network.fhost': { + category: 'rsa', + name: 'rsa.network.fhost', + type: 'keyword', + }, + 'rsa.network.fport': { + category: 'rsa', + name: 'rsa.network.fport', + type: 'keyword', + }, + 'rsa.network.laddr': { + category: 'rsa', + name: 'rsa.network.laddr', + type: 'keyword', + }, + 'rsa.network.linterface': { + category: 'rsa', + name: 'rsa.network.linterface', + type: 'keyword', + }, + 'rsa.network.phost': { + category: 'rsa', + name: 'rsa.network.phost', + type: 'keyword', + }, + 'rsa.network.ad_computer_dst': { + category: 'rsa', + description: 'Deprecated, use host.dst', + name: 'rsa.network.ad_computer_dst', + type: 'keyword', + }, + 'rsa.network.eth_type': { + category: 'rsa', + description: 'This key is used to capture Ethernet Type, Used for Layer 3 Protocols Only', + name: 'rsa.network.eth_type', + type: 'long', + }, + 'rsa.network.ip_proto': { + category: 'rsa', + description: + 'This key should be used to capture the Protocol number, all the protocol nubers are converted into string in UI', + name: 'rsa.network.ip_proto', + type: 'long', + }, + 'rsa.network.dns_cname_record': { + category: 'rsa', + name: 'rsa.network.dns_cname_record', + type: 'keyword', + }, + 'rsa.network.dns_id': { + category: 'rsa', + name: 'rsa.network.dns_id', + type: 'keyword', + }, + 'rsa.network.dns_opcode': { + category: 'rsa', + name: 'rsa.network.dns_opcode', + type: 'keyword', + }, + 'rsa.network.dns_resp': { + category: 'rsa', + name: 'rsa.network.dns_resp', + type: 'keyword', + }, + 'rsa.network.dns_type': { + category: 'rsa', + name: 'rsa.network.dns_type', + type: 'keyword', + }, + 'rsa.network.domain1': { + category: 'rsa', + name: 'rsa.network.domain1', + type: 'keyword', + }, + 'rsa.network.host_type': { + category: 'rsa', + name: 'rsa.network.host_type', + type: 'keyword', + }, + 'rsa.network.packet_length': { + category: 'rsa', + name: 'rsa.network.packet_length', + type: 'keyword', + }, + 'rsa.network.host_orig': { + category: 'rsa', + description: + 'This is used to capture the original hostname in case of a Forwarding Agent or a Proxy in between.', + name: 'rsa.network.host_orig', + type: 'keyword', + }, + 'rsa.network.rpayload': { + category: 'rsa', + description: + 'This key is used to capture the total number of payload bytes seen in the retransmitted packets.', + name: 'rsa.network.rpayload', + type: 'keyword', + }, + 'rsa.network.vlan_name': { + category: 'rsa', + description: 'This key should only be used to capture the name of the Virtual LAN', + name: 'rsa.network.vlan_name', + type: 'keyword', + }, + 'rsa.investigations.ec_activity': { + category: 'rsa', + description: 'This key captures the particular event activity(Ex:Logoff)', + name: 'rsa.investigations.ec_activity', + type: 'keyword', + }, + 'rsa.investigations.ec_theme': { + category: 'rsa', + description: 'This key captures the Theme of a particular Event(Ex:Authentication)', + name: 'rsa.investigations.ec_theme', + type: 'keyword', + }, + 'rsa.investigations.ec_subject': { + category: 'rsa', + description: 'This key captures the Subject of a particular Event(Ex:User)', + name: 'rsa.investigations.ec_subject', + type: 'keyword', + }, + 'rsa.investigations.ec_outcome': { + category: 'rsa', + description: 'This key captures the outcome of a particular Event(Ex:Success)', + name: 'rsa.investigations.ec_outcome', + type: 'keyword', + }, + 'rsa.investigations.event_cat': { + category: 'rsa', + description: 'This key captures the Event category number', + name: 'rsa.investigations.event_cat', + type: 'long', + }, + 'rsa.investigations.event_cat_name': { + category: 'rsa', + description: 'This key captures the event category name corresponding to the event cat code', + name: 'rsa.investigations.event_cat_name', + type: 'keyword', + }, + 'rsa.investigations.event_vcat': { + category: 'rsa', + description: + 'This is a vendor supplied category. This should be used in situations where the vendor has adopted their own event_category taxonomy.', + name: 'rsa.investigations.event_vcat', + type: 'keyword', + }, + 'rsa.investigations.analysis_file': { + category: 'rsa', + description: + 'This is used to capture all indicators used in a File Analysis. This key should be used to capture an analysis of a file', + name: 'rsa.investigations.analysis_file', + type: 'keyword', + }, + 'rsa.investigations.analysis_service': { + category: 'rsa', + description: + 'This is used to capture all indicators used in a Service Analysis. This key should be used to capture an analysis of a service', + name: 'rsa.investigations.analysis_service', + type: 'keyword', + }, + 'rsa.investigations.analysis_session': { + category: 'rsa', + description: + 'This is used to capture all indicators used for a Session Analysis. This key should be used to capture an analysis of a session', + name: 'rsa.investigations.analysis_session', + type: 'keyword', + }, + 'rsa.investigations.boc': { + category: 'rsa', + description: 'This is used to capture behaviour of compromise', + name: 'rsa.investigations.boc', + type: 'keyword', + }, + 'rsa.investigations.eoc': { + category: 'rsa', + description: 'This is used to capture Enablers of Compromise', + name: 'rsa.investigations.eoc', + type: 'keyword', + }, + 'rsa.investigations.inv_category': { + category: 'rsa', + description: 'This used to capture investigation category', + name: 'rsa.investigations.inv_category', + type: 'keyword', + }, + 'rsa.investigations.inv_context': { + category: 'rsa', + description: 'This used to capture investigation context', + name: 'rsa.investigations.inv_context', + type: 'keyword', + }, + 'rsa.investigations.ioc': { + category: 'rsa', + description: 'This is key capture indicator of compromise', + name: 'rsa.investigations.ioc', + type: 'keyword', + }, + 'rsa.counters.dclass_c1': { + category: 'rsa', + description: + 'This is a generic counter key that should be used with the label dclass.c1.str only', + name: 'rsa.counters.dclass_c1', + type: 'long', + }, + 'rsa.counters.dclass_c2': { + category: 'rsa', + description: + 'This is a generic counter key that should be used with the label dclass.c2.str only', + name: 'rsa.counters.dclass_c2', + type: 'long', + }, + 'rsa.counters.event_counter': { + category: 'rsa', + description: 'This is used to capture the number of times an event repeated', + name: 'rsa.counters.event_counter', + type: 'long', + }, + 'rsa.counters.dclass_r1': { + category: 'rsa', + description: + 'This is a generic ratio key that should be used with the label dclass.r1.str only', + name: 'rsa.counters.dclass_r1', + type: 'keyword', + }, + 'rsa.counters.dclass_c3': { + category: 'rsa', + description: + 'This is a generic counter key that should be used with the label dclass.c3.str only', + name: 'rsa.counters.dclass_c3', + type: 'long', + }, + 'rsa.counters.dclass_c1_str': { + category: 'rsa', + description: + 'This is a generic counter string key that should be used with the label dclass.c1 only', + name: 'rsa.counters.dclass_c1_str', + type: 'keyword', + }, + 'rsa.counters.dclass_c2_str': { + category: 'rsa', + description: + 'This is a generic counter string key that should be used with the label dclass.c2 only', + name: 'rsa.counters.dclass_c2_str', + type: 'keyword', + }, + 'rsa.counters.dclass_r1_str': { + category: 'rsa', + description: + 'This is a generic ratio string key that should be used with the label dclass.r1 only', + name: 'rsa.counters.dclass_r1_str', + type: 'keyword', + }, + 'rsa.counters.dclass_r2': { + category: 'rsa', + description: + 'This is a generic ratio key that should be used with the label dclass.r2.str only', + name: 'rsa.counters.dclass_r2', + type: 'keyword', + }, + 'rsa.counters.dclass_c3_str': { + category: 'rsa', + description: + 'This is a generic counter string key that should be used with the label dclass.c3 only', + name: 'rsa.counters.dclass_c3_str', + type: 'keyword', + }, + 'rsa.counters.dclass_r3': { + category: 'rsa', + description: + 'This is a generic ratio key that should be used with the label dclass.r3.str only', + name: 'rsa.counters.dclass_r3', + type: 'keyword', + }, + 'rsa.counters.dclass_r2_str': { + category: 'rsa', + description: + 'This is a generic ratio string key that should be used with the label dclass.r2 only', + name: 'rsa.counters.dclass_r2_str', + type: 'keyword', + }, + 'rsa.counters.dclass_r3_str': { + category: 'rsa', + description: + 'This is a generic ratio string key that should be used with the label dclass.r3 only', + name: 'rsa.counters.dclass_r3_str', + type: 'keyword', + }, + 'rsa.identity.auth_method': { + category: 'rsa', + description: 'This key is used to capture authentication methods used only', + name: 'rsa.identity.auth_method', + type: 'keyword', + }, + 'rsa.identity.user_role': { + category: 'rsa', + description: 'This key is used to capture the Role of a user only', + name: 'rsa.identity.user_role', + type: 'keyword', + }, + 'rsa.identity.dn': { + category: 'rsa', + description: 'X.500 (LDAP) Distinguished Name', + name: 'rsa.identity.dn', + type: 'keyword', + }, + 'rsa.identity.logon_type': { + category: 'rsa', + description: 'This key is used to capture the type of logon method used.', + name: 'rsa.identity.logon_type', + type: 'keyword', + }, + 'rsa.identity.profile': { + category: 'rsa', + description: 'This key is used to capture the user profile', + name: 'rsa.identity.profile', + type: 'keyword', + }, + 'rsa.identity.accesses': { + category: 'rsa', + description: 'This key is used to capture actual privileges used in accessing an object', + name: 'rsa.identity.accesses', + type: 'keyword', + }, + 'rsa.identity.realm': { + category: 'rsa', + description: 'Radius realm or similar grouping of accounts', + name: 'rsa.identity.realm', + type: 'keyword', + }, + 'rsa.identity.user_sid_dst': { + category: 'rsa', + description: 'This key captures Destination User Session ID', + name: 'rsa.identity.user_sid_dst', + type: 'keyword', + }, + 'rsa.identity.dn_src': { + category: 'rsa', + description: + 'An X.500 (LDAP) Distinguished name that is used in a context that indicates a Source dn', + name: 'rsa.identity.dn_src', + type: 'keyword', + }, + 'rsa.identity.org': { + category: 'rsa', + description: 'This key captures the User organization', + name: 'rsa.identity.org', + type: 'keyword', + }, + 'rsa.identity.dn_dst': { + category: 'rsa', + description: + 'An X.500 (LDAP) Distinguished name that used in a context that indicates a Destination dn', + name: 'rsa.identity.dn_dst', + type: 'keyword', + }, + 'rsa.identity.firstname': { + category: 'rsa', + description: + 'This key is for First Names only, this is used for Healthcare predominantly to capture Patients information', + name: 'rsa.identity.firstname', + type: 'keyword', + }, + 'rsa.identity.lastname': { + category: 'rsa', + description: + 'This key is for Last Names only, this is used for Healthcare predominantly to capture Patients information', + name: 'rsa.identity.lastname', + type: 'keyword', + }, + 'rsa.identity.user_dept': { + category: 'rsa', + description: "User's Department Names only", + name: 'rsa.identity.user_dept', + type: 'keyword', + }, + 'rsa.identity.user_sid_src': { + category: 'rsa', + description: 'This key captures Source User Session ID', + name: 'rsa.identity.user_sid_src', + type: 'keyword', + }, + 'rsa.identity.federated_sp': { + category: 'rsa', + description: + 'This key is the Federated Service Provider. This is the application requesting authentication.', + name: 'rsa.identity.federated_sp', + type: 'keyword', + }, + 'rsa.identity.federated_idp': { + category: 'rsa', + description: + 'This key is the federated Identity Provider. This is the server providing the authentication.', + name: 'rsa.identity.federated_idp', + type: 'keyword', + }, + 'rsa.identity.logon_type_desc': { + category: 'rsa', + description: + "This key is used to capture the textual description of an integer logon type as stored in the meta key 'logon.type'.", + name: 'rsa.identity.logon_type_desc', + type: 'keyword', + }, + 'rsa.identity.middlename': { + category: 'rsa', + description: + 'This key is for Middle Names only, this is used for Healthcare predominantly to capture Patients information', + name: 'rsa.identity.middlename', + type: 'keyword', + }, + 'rsa.identity.password': { + category: 'rsa', + description: 'This key is for Passwords seen in any session, plain text or encrypted', + name: 'rsa.identity.password', + type: 'keyword', + }, + 'rsa.identity.host_role': { + category: 'rsa', + description: 'This key should only be used to capture the role of a Host Machine', + name: 'rsa.identity.host_role', + type: 'keyword', + }, + 'rsa.identity.ldap': { + category: 'rsa', + description: + 'This key is for Uninterpreted LDAP values. Ldap Values that don’t have a clear query or response context', + name: 'rsa.identity.ldap', + type: 'keyword', + }, + 'rsa.identity.ldap_query': { + category: 'rsa', + description: 'This key is the Search criteria from an LDAP search', + name: 'rsa.identity.ldap_query', + type: 'keyword', + }, + 'rsa.identity.ldap_response': { + category: 'rsa', + description: 'This key is to capture Results from an LDAP search', + name: 'rsa.identity.ldap_response', + type: 'keyword', + }, + 'rsa.identity.owner': { + category: 'rsa', + description: + 'This is used to capture username the process or service is running as, the author of the task', + name: 'rsa.identity.owner', + type: 'keyword', + }, + 'rsa.identity.service_account': { + category: 'rsa', + description: + 'This key is a windows specific key, used for capturing name of the account a service (referenced in the event) is running under. Legacy Usage', + name: 'rsa.identity.service_account', + type: 'keyword', + }, + 'rsa.email.email_dst': { + category: 'rsa', + description: + 'This key is used to capture the Destination email address only, when the destination context is not clear use email', + name: 'rsa.email.email_dst', + type: 'keyword', + }, + 'rsa.email.email_src': { + category: 'rsa', + description: + 'This key is used to capture the source email address only, when the source context is not clear use email', + name: 'rsa.email.email_src', + type: 'keyword', + }, + 'rsa.email.subject': { + category: 'rsa', + description: 'This key is used to capture the subject string from an Email only.', + name: 'rsa.email.subject', + type: 'keyword', + }, + 'rsa.email.email': { + category: 'rsa', + description: + 'This key is used to capture a generic email address where the source or destination context is not clear', + name: 'rsa.email.email', + type: 'keyword', + }, + 'rsa.email.trans_from': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.email.trans_from', + type: 'keyword', + }, + 'rsa.email.trans_to': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.email.trans_to', + type: 'keyword', + }, + 'rsa.file.privilege': { + category: 'rsa', + description: 'Deprecated, use permissions', + name: 'rsa.file.privilege', + type: 'keyword', + }, + 'rsa.file.attachment': { + category: 'rsa', + description: 'This key captures the attachment file name', + name: 'rsa.file.attachment', + type: 'keyword', + }, + 'rsa.file.filesystem': { + category: 'rsa', + name: 'rsa.file.filesystem', + type: 'keyword', + }, + 'rsa.file.binary': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.file.binary', + type: 'keyword', + }, + 'rsa.file.filename_dst': { + category: 'rsa', + description: 'This is used to capture name of the file targeted by the action', + name: 'rsa.file.filename_dst', + type: 'keyword', + }, + 'rsa.file.filename_src': { + category: 'rsa', + description: + 'This is used to capture name of the parent filename, the file which performed the action', + name: 'rsa.file.filename_src', + type: 'keyword', + }, + 'rsa.file.filename_tmp': { + category: 'rsa', + name: 'rsa.file.filename_tmp', + type: 'keyword', + }, + 'rsa.file.directory_dst': { + category: 'rsa', + description: + 'This key is used to capture the directory of the target process or file', + name: 'rsa.file.directory_dst', + type: 'keyword', + }, + 'rsa.file.directory_src': { + category: 'rsa', + description: 'This key is used to capture the directory of the source process or file', + name: 'rsa.file.directory_src', + type: 'keyword', + }, + 'rsa.file.file_entropy': { + category: 'rsa', + description: 'This is used to capture entropy vale of a file', + name: 'rsa.file.file_entropy', + type: 'double', + }, + 'rsa.file.file_vendor': { + category: 'rsa', + description: 'This is used to capture Company name of file located in version_info', + name: 'rsa.file.file_vendor', + type: 'keyword', + }, + 'rsa.file.task_name': { + category: 'rsa', + description: 'This is used to capture name of the task', + name: 'rsa.file.task_name', + type: 'keyword', + }, + 'rsa.web.fqdn': { + category: 'rsa', + description: 'Fully Qualified Domain Names', + name: 'rsa.web.fqdn', + type: 'keyword', + }, + 'rsa.web.web_cookie': { + category: 'rsa', + description: 'This key is used to capture the Web cookies specifically.', + name: 'rsa.web.web_cookie', + type: 'keyword', + }, + 'rsa.web.alias_host': { + category: 'rsa', + name: 'rsa.web.alias_host', + type: 'keyword', + }, + 'rsa.web.reputation_num': { + category: 'rsa', + description: 'Reputation Number of an entity. Typically used for Web Domains', + name: 'rsa.web.reputation_num', + type: 'double', + }, + 'rsa.web.web_ref_domain': { + category: 'rsa', + description: "Web referer's domain", + name: 'rsa.web.web_ref_domain', + type: 'keyword', + }, + 'rsa.web.web_ref_query': { + category: 'rsa', + description: "This key captures Web referer's query portion of the URL", + name: 'rsa.web.web_ref_query', + type: 'keyword', + }, + 'rsa.web.remote_domain': { + category: 'rsa', + name: 'rsa.web.remote_domain', + type: 'keyword', + }, + 'rsa.web.web_ref_page': { + category: 'rsa', + description: "This key captures Web referer's page information", + name: 'rsa.web.web_ref_page', + type: 'keyword', + }, + 'rsa.web.web_ref_root': { + category: 'rsa', + description: "Web referer's root URL path", + name: 'rsa.web.web_ref_root', + type: 'keyword', + }, + 'rsa.web.cn_asn_dst': { + category: 'rsa', + name: 'rsa.web.cn_asn_dst', + type: 'keyword', + }, + 'rsa.web.cn_rpackets': { + category: 'rsa', + name: 'rsa.web.cn_rpackets', + type: 'keyword', + }, + 'rsa.web.urlpage': { + category: 'rsa', + name: 'rsa.web.urlpage', + type: 'keyword', + }, + 'rsa.web.urlroot': { + category: 'rsa', + name: 'rsa.web.urlroot', + type: 'keyword', + }, + 'rsa.web.p_url': { + category: 'rsa', + name: 'rsa.web.p_url', + type: 'keyword', + }, + 'rsa.web.p_user_agent': { + category: 'rsa', + name: 'rsa.web.p_user_agent', + type: 'keyword', + }, + 'rsa.web.p_web_cookie': { + category: 'rsa', + name: 'rsa.web.p_web_cookie', + type: 'keyword', + }, + 'rsa.web.p_web_method': { + category: 'rsa', + name: 'rsa.web.p_web_method', + type: 'keyword', + }, + 'rsa.web.p_web_referer': { + category: 'rsa', + name: 'rsa.web.p_web_referer', + type: 'keyword', + }, + 'rsa.web.web_extension_tmp': { + category: 'rsa', + name: 'rsa.web.web_extension_tmp', + type: 'keyword', + }, + 'rsa.web.web_page': { + category: 'rsa', + name: 'rsa.web.web_page', + type: 'keyword', + }, + 'rsa.threat.threat_category': { + category: 'rsa', + description: 'This key captures Threat Name/Threat Category/Categorization of alert', + name: 'rsa.threat.threat_category', + type: 'keyword', + }, + 'rsa.threat.threat_desc': { + category: 'rsa', + description: + 'This key is used to capture the threat description from the session directly or inferred', + name: 'rsa.threat.threat_desc', + type: 'keyword', + }, + 'rsa.threat.alert': { + category: 'rsa', + description: 'This key is used to capture name of the alert', + name: 'rsa.threat.alert', + type: 'keyword', + }, + 'rsa.threat.threat_source': { + category: 'rsa', + description: 'This key is used to capture source of the threat', + name: 'rsa.threat.threat_source', + type: 'keyword', + }, + 'rsa.crypto.crypto': { + category: 'rsa', + description: 'This key is used to capture the Encryption Type or Encryption Key only', + name: 'rsa.crypto.crypto', + type: 'keyword', + }, + 'rsa.crypto.cipher_src': { + category: 'rsa', + description: 'This key is for Source (Client) Cipher', + name: 'rsa.crypto.cipher_src', + type: 'keyword', + }, + 'rsa.crypto.cert_subject': { + category: 'rsa', + description: 'This key is used to capture the Certificate organization only', + name: 'rsa.crypto.cert_subject', + type: 'keyword', + }, + 'rsa.crypto.peer': { + category: 'rsa', + description: "This key is for Encryption peer's IP Address", + name: 'rsa.crypto.peer', + type: 'keyword', + }, + 'rsa.crypto.cipher_size_src': { + category: 'rsa', + description: 'This key captures Source (Client) Cipher Size', + name: 'rsa.crypto.cipher_size_src', + type: 'long', + }, + 'rsa.crypto.ike': { + category: 'rsa', + description: 'IKE negotiation phase.', + name: 'rsa.crypto.ike', + type: 'keyword', + }, + 'rsa.crypto.scheme': { + category: 'rsa', + description: 'This key captures the Encryption scheme used', + name: 'rsa.crypto.scheme', + type: 'keyword', + }, + 'rsa.crypto.peer_id': { + category: 'rsa', + description: 'This key is for Encryption peer’s identity', + name: 'rsa.crypto.peer_id', + type: 'keyword', + }, + 'rsa.crypto.sig_type': { + category: 'rsa', + description: 'This key captures the Signature Type', + name: 'rsa.crypto.sig_type', + type: 'keyword', + }, + 'rsa.crypto.cert_issuer': { + category: 'rsa', + name: 'rsa.crypto.cert_issuer', + type: 'keyword', + }, + 'rsa.crypto.cert_host_name': { + category: 'rsa', + description: 'Deprecated key defined only in table map.', + name: 'rsa.crypto.cert_host_name', + type: 'keyword', + }, + 'rsa.crypto.cert_error': { + category: 'rsa', + description: 'This key captures the Certificate Error String', + name: 'rsa.crypto.cert_error', + type: 'keyword', + }, + 'rsa.crypto.cipher_dst': { + category: 'rsa', + description: 'This key is for Destination (Server) Cipher', + name: 'rsa.crypto.cipher_dst', + type: 'keyword', + }, + 'rsa.crypto.cipher_size_dst': { + category: 'rsa', + description: 'This key captures Destination (Server) Cipher Size', + name: 'rsa.crypto.cipher_size_dst', + type: 'long', + }, + 'rsa.crypto.ssl_ver_src': { + category: 'rsa', + description: 'Deprecated, use version', + name: 'rsa.crypto.ssl_ver_src', + type: 'keyword', + }, + 'rsa.crypto.d_certauth': { + category: 'rsa', + name: 'rsa.crypto.d_certauth', + type: 'keyword', + }, + 'rsa.crypto.s_certauth': { + category: 'rsa', + name: 'rsa.crypto.s_certauth', + type: 'keyword', + }, + 'rsa.crypto.ike_cookie1': { + category: 'rsa', + description: 'ID of the negotiation — sent for ISAKMP Phase One', + name: 'rsa.crypto.ike_cookie1', + type: 'keyword', + }, + 'rsa.crypto.ike_cookie2': { + category: 'rsa', + description: 'ID of the negotiation — sent for ISAKMP Phase Two', + name: 'rsa.crypto.ike_cookie2', + type: 'keyword', + }, + 'rsa.crypto.cert_checksum': { + category: 'rsa', + name: 'rsa.crypto.cert_checksum', + type: 'keyword', + }, + 'rsa.crypto.cert_host_cat': { + category: 'rsa', + description: 'This key is used for the hostname category value of a certificate', + name: 'rsa.crypto.cert_host_cat', + type: 'keyword', + }, + 'rsa.crypto.cert_serial': { + category: 'rsa', + description: 'This key is used to capture the Certificate serial number only', + name: 'rsa.crypto.cert_serial', + type: 'keyword', + }, + 'rsa.crypto.cert_status': { + category: 'rsa', + description: 'This key captures Certificate validation status', + name: 'rsa.crypto.cert_status', + type: 'keyword', + }, + 'rsa.crypto.ssl_ver_dst': { + category: 'rsa', + description: 'Deprecated, use version', + name: 'rsa.crypto.ssl_ver_dst', + type: 'keyword', + }, + 'rsa.crypto.cert_keysize': { + category: 'rsa', + name: 'rsa.crypto.cert_keysize', + type: 'keyword', + }, + 'rsa.crypto.cert_username': { + category: 'rsa', + name: 'rsa.crypto.cert_username', + type: 'keyword', + }, + 'rsa.crypto.https_insact': { + category: 'rsa', + name: 'rsa.crypto.https_insact', + type: 'keyword', + }, + 'rsa.crypto.https_valid': { + category: 'rsa', + name: 'rsa.crypto.https_valid', + type: 'keyword', + }, + 'rsa.crypto.cert_ca': { + category: 'rsa', + description: 'This key is used to capture the Certificate signing authority only', + name: 'rsa.crypto.cert_ca', + type: 'keyword', + }, + 'rsa.crypto.cert_common': { + category: 'rsa', + description: 'This key is used to capture the Certificate common name only', + name: 'rsa.crypto.cert_common', + type: 'keyword', + }, + 'rsa.wireless.wlan_ssid': { + category: 'rsa', + description: 'This key is used to capture the ssid of a Wireless Session', + name: 'rsa.wireless.wlan_ssid', + type: 'keyword', + }, + 'rsa.wireless.access_point': { + category: 'rsa', + description: 'This key is used to capture the access point name.', + name: 'rsa.wireless.access_point', + type: 'keyword', + }, + 'rsa.wireless.wlan_channel': { + category: 'rsa', + description: 'This is used to capture the channel names', + name: 'rsa.wireless.wlan_channel', + type: 'long', + }, + 'rsa.wireless.wlan_name': { + category: 'rsa', + description: 'This key captures either WLAN number/name', + name: 'rsa.wireless.wlan_name', + type: 'keyword', + }, + 'rsa.storage.disk_volume': { + category: 'rsa', + description: 'A unique name assigned to logical units (volumes) within a physical disk', + name: 'rsa.storage.disk_volume', + type: 'keyword', + }, + 'rsa.storage.lun': { + category: 'rsa', + description: 'Logical Unit Number.This key is a very useful concept in Storage.', + name: 'rsa.storage.lun', + type: 'keyword', + }, + 'rsa.storage.pwwn': { + category: 'rsa', + description: 'This uniquely identifies a port on a HBA.', + name: 'rsa.storage.pwwn', + type: 'keyword', + }, + 'rsa.physical.org_dst': { + category: 'rsa', + description: + 'This is used to capture the destination organization based on the GEOPIP Maxmind database.', + name: 'rsa.physical.org_dst', + type: 'keyword', + }, + 'rsa.physical.org_src': { + category: 'rsa', + description: + 'This is used to capture the source organization based on the GEOPIP Maxmind database.', + name: 'rsa.physical.org_src', + type: 'keyword', + }, + 'rsa.healthcare.patient_fname': { + category: 'rsa', + description: + 'This key is for First Names only, this is used for Healthcare predominantly to capture Patients information', + name: 'rsa.healthcare.patient_fname', + type: 'keyword', + }, + 'rsa.healthcare.patient_id': { + category: 'rsa', + description: 'This key captures the unique ID for a patient', + name: 'rsa.healthcare.patient_id', + type: 'keyword', + }, + 'rsa.healthcare.patient_lname': { + category: 'rsa', + description: + 'This key is for Last Names only, this is used for Healthcare predominantly to capture Patients information', + name: 'rsa.healthcare.patient_lname', + type: 'keyword', + }, + 'rsa.healthcare.patient_mname': { + category: 'rsa', + description: + 'This key is for Middle Names only, this is used for Healthcare predominantly to capture Patients information', + name: 'rsa.healthcare.patient_mname', + type: 'keyword', + }, + 'rsa.endpoint.host_state': { + category: 'rsa', + description: + 'This key is used to capture the current state of the machine, such as blacklisted, infected, firewall disabled and so on', + name: 'rsa.endpoint.host_state', + type: 'keyword', + }, + 'rsa.endpoint.registry_key': { + category: 'rsa', + description: 'This key captures the path to the registry key', + name: 'rsa.endpoint.registry_key', + type: 'keyword', + }, + 'rsa.endpoint.registry_value': { + category: 'rsa', + description: 'This key captures values or decorators used within a registry entry', + name: 'rsa.endpoint.registry_value', + type: 'keyword', + }, + 'forcepoint.virus_id': { + category: 'forcepoint', + description: 'Virus ID ', + name: 'forcepoint.virus_id', + type: 'keyword', + }, + 'checkpoint.app_risk': { + category: 'checkpoint', + description: 'Application risk.', + name: 'checkpoint.app_risk', + type: 'keyword', + }, + 'checkpoint.app_severity': { + category: 'checkpoint', + description: 'Application threat severity.', + name: 'checkpoint.app_severity', + type: 'keyword', + }, + 'checkpoint.app_sig_id': { + category: 'checkpoint', + description: 'The signature ID which the application was detected by.', + name: 'checkpoint.app_sig_id', + type: 'keyword', + }, + 'checkpoint.auth_method': { + category: 'checkpoint', + description: 'Password authentication protocol used.', + name: 'checkpoint.auth_method', + type: 'keyword', + }, + 'checkpoint.category': { + category: 'checkpoint', + description: 'Category.', + name: 'checkpoint.category', + type: 'keyword', + }, + 'checkpoint.confidence_level': { + category: 'checkpoint', + description: 'Confidence level determined.', + name: 'checkpoint.confidence_level', + type: 'integer', + }, + 'checkpoint.connectivity_state': { + category: 'checkpoint', + description: 'Connectivity state.', + name: 'checkpoint.connectivity_state', + type: 'keyword', + }, + 'checkpoint.cookie': { + category: 'checkpoint', + description: 'IKE cookie.', + name: 'checkpoint.cookie', + type: 'keyword', + }, + 'checkpoint.dst_phone_number': { + category: 'checkpoint', + description: 'Destination IP-Phone.', + name: 'checkpoint.dst_phone_number', + type: 'keyword', + }, + 'checkpoint.email_control': { + category: 'checkpoint', + description: 'Engine name.', + name: 'checkpoint.email_control', + type: 'keyword', + }, + 'checkpoint.email_id': { + category: 'checkpoint', + description: 'Internal email ID.', + name: 'checkpoint.email_id', + type: 'keyword', + }, + 'checkpoint.email_recipients_num': { + category: 'checkpoint', + description: 'Number of recipients.', + name: 'checkpoint.email_recipients_num', + type: 'long', + }, + 'checkpoint.email_session_id': { + category: 'checkpoint', + description: 'Internal email session ID.', + name: 'checkpoint.email_session_id', + type: 'keyword', + }, + 'checkpoint.email_spool_id': { + category: 'checkpoint', + description: 'Internal email spool ID.', + name: 'checkpoint.email_spool_id', + type: 'keyword', + }, + 'checkpoint.email_subject': { + category: 'checkpoint', + description: 'Email subject.', + name: 'checkpoint.email_subject', + type: 'keyword', + }, + 'checkpoint.event_count': { + category: 'checkpoint', + description: 'Number of events associated with the log.', + name: 'checkpoint.event_count', + type: 'long', + }, + 'checkpoint.frequency': { + category: 'checkpoint', + description: 'Scan frequency.', + name: 'checkpoint.frequency', + type: 'keyword', + }, + 'checkpoint.icmp_type': { + category: 'checkpoint', + description: 'ICMP type.', + name: 'checkpoint.icmp_type', + type: 'long', + }, + 'checkpoint.icmp_code': { + category: 'checkpoint', + description: 'ICMP code.', + name: 'checkpoint.icmp_code', + type: 'long', + }, + 'checkpoint.identity_type': { + category: 'checkpoint', + description: 'Identity type.', + name: 'checkpoint.identity_type', + type: 'keyword', + }, + 'checkpoint.incident_extension': { + category: 'checkpoint', + description: 'Format of original data.', + name: 'checkpoint.incident_extension', + type: 'keyword', + }, + 'checkpoint.integrity_av_invoke_type': { + category: 'checkpoint', + description: 'Scan invoke type.', + name: 'checkpoint.integrity_av_invoke_type', + type: 'keyword', + }, + 'checkpoint.malware_family': { + category: 'checkpoint', + description: 'Malware family.', + name: 'checkpoint.malware_family', + type: 'keyword', + }, + 'checkpoint.peer_gateway': { + category: 'checkpoint', + description: 'Main IP of the peer Security Gateway.', + name: 'checkpoint.peer_gateway', + type: 'ip', + }, + 'checkpoint.performance_impact': { + category: 'checkpoint', + description: 'Protection performance impact.', + name: 'checkpoint.performance_impact', + type: 'integer', + }, + 'checkpoint.protection_id': { + category: 'checkpoint', + description: 'Protection malware ID.', + name: 'checkpoint.protection_id', + type: 'keyword', + }, + 'checkpoint.protection_name': { + category: 'checkpoint', + description: 'Specific signature name of the attack.', + name: 'checkpoint.protection_name', + type: 'keyword', + }, + 'checkpoint.protection_type': { + category: 'checkpoint', + description: 'Type of protection used to detect the attack.', + name: 'checkpoint.protection_type', + type: 'keyword', + }, + 'checkpoint.scan_result': { + category: 'checkpoint', + description: 'Scan result.', + name: 'checkpoint.scan_result', + type: 'keyword', + }, + 'checkpoint.sensor_mode': { + category: 'checkpoint', + description: 'Sensor mode.', + name: 'checkpoint.sensor_mode', + type: 'keyword', + }, + 'checkpoint.severity': { + category: 'checkpoint', + description: 'Threat severity.', + name: 'checkpoint.severity', + type: 'keyword', + }, + 'checkpoint.spyware_name': { + category: 'checkpoint', + description: 'Spyware name.', + name: 'checkpoint.spyware_name', + type: 'keyword', + }, + 'checkpoint.spyware_status': { + category: 'checkpoint', + description: 'Spyware status.', + name: 'checkpoint.spyware_status', + type: 'keyword', + }, + 'checkpoint.subs_exp': { + category: 'checkpoint', + description: 'The expiration date of the subscription.', + name: 'checkpoint.subs_exp', + type: 'date', + }, + 'checkpoint.tcp_flags': { + category: 'checkpoint', + description: 'TCP packet flags.', + name: 'checkpoint.tcp_flags', + type: 'keyword', + }, + 'checkpoint.termination_reason': { + category: 'checkpoint', + description: 'Termination reason.', + name: 'checkpoint.termination_reason', + type: 'keyword', + }, + 'checkpoint.update_status': { + category: 'checkpoint', + description: 'Update status.', + name: 'checkpoint.update_status', + type: 'keyword', + }, + 'checkpoint.user_status': { + category: 'checkpoint', + description: 'User response.', + name: 'checkpoint.user_status', + type: 'keyword', + }, + 'checkpoint.uuid': { + category: 'checkpoint', + description: 'External ID.', + name: 'checkpoint.uuid', + type: 'keyword', + }, + 'checkpoint.virus_name': { + category: 'checkpoint', + description: 'Virus name.', + name: 'checkpoint.virus_name', + type: 'keyword', + }, + 'checkpoint.voip_log_type': { + category: 'checkpoint', + description: 'VoIP log types.', + name: 'checkpoint.voip_log_type', + type: 'keyword', + }, + 'cef.extensions.cp_app_risk': { + category: 'cef', + name: 'cef.extensions.cp_app_risk', + type: 'keyword', + }, + 'cef.extensions.cp_severity': { + category: 'cef', + name: 'cef.extensions.cp_severity', + type: 'keyword', + }, + 'cef.extensions.ifname': { + category: 'cef', + name: 'cef.extensions.ifname', + type: 'keyword', + }, + 'cef.extensions.inzone': { + category: 'cef', + name: 'cef.extensions.inzone', + type: 'keyword', + }, + 'cef.extensions.layer_uuid': { + category: 'cef', + name: 'cef.extensions.layer_uuid', + type: 'keyword', + }, + 'cef.extensions.layer_name': { + category: 'cef', + name: 'cef.extensions.layer_name', + type: 'keyword', + }, + 'cef.extensions.logid': { + category: 'cef', + name: 'cef.extensions.logid', + type: 'keyword', + }, + 'cef.extensions.loguid': { + category: 'cef', + name: 'cef.extensions.loguid', + type: 'keyword', + }, + 'cef.extensions.match_id': { + category: 'cef', + name: 'cef.extensions.match_id', + type: 'keyword', + }, + 'cef.extensions.nat_addtnl_rulenum': { + category: 'cef', + name: 'cef.extensions.nat_addtnl_rulenum', + type: 'keyword', + }, + 'cef.extensions.nat_rulenum': { + category: 'cef', + name: 'cef.extensions.nat_rulenum', + type: 'keyword', + }, + 'cef.extensions.origin': { + category: 'cef', + name: 'cef.extensions.origin', + type: 'keyword', + }, + 'cef.extensions.originsicname': { + category: 'cef', + name: 'cef.extensions.originsicname', + type: 'keyword', + }, + 'cef.extensions.outzone': { + category: 'cef', + name: 'cef.extensions.outzone', + type: 'keyword', + }, + 'cef.extensions.parent_rule': { + category: 'cef', + name: 'cef.extensions.parent_rule', + type: 'keyword', + }, + 'cef.extensions.product': { + category: 'cef', + name: 'cef.extensions.product', + type: 'keyword', + }, + 'cef.extensions.rule_action': { + category: 'cef', + name: 'cef.extensions.rule_action', + type: 'keyword', + }, + 'cef.extensions.rule_uid': { + category: 'cef', + name: 'cef.extensions.rule_uid', + type: 'keyword', + }, + 'cef.extensions.sequencenum': { + category: 'cef', + name: 'cef.extensions.sequencenum', + type: 'keyword', + }, + 'cef.extensions.service_id': { + category: 'cef', + name: 'cef.extensions.service_id', + type: 'keyword', + }, + 'cef.extensions.version': { + category: 'cef', + name: 'cef.extensions.version', + type: 'keyword', + }, + 'checkpoint.calc_desc': { + category: 'checkpoint', + description: 'Log description. ', + name: 'checkpoint.calc_desc', + type: 'keyword', + }, + 'checkpoint.dst_country': { + category: 'checkpoint', + description: 'Destination country. ', + name: 'checkpoint.dst_country', + type: 'keyword', + }, + 'checkpoint.dst_user_name': { + category: 'checkpoint', + description: 'Connected user name on the destination IP. ', + name: 'checkpoint.dst_user_name', + type: 'keyword', + }, + 'checkpoint.sys_message': { + category: 'checkpoint', + description: 'System messages ', + name: 'checkpoint.sys_message', + type: 'keyword', + }, + 'checkpoint.logid': { + category: 'checkpoint', + description: 'System messages ', + name: 'checkpoint.logid', + type: 'keyword', + }, + 'checkpoint.failure_impact': { + category: 'checkpoint', + description: 'The impact of update service failure. ', + name: 'checkpoint.failure_impact', + type: 'keyword', + }, + 'checkpoint.id': { + category: 'checkpoint', + description: 'Override application ID. ', + name: 'checkpoint.id', + type: 'integer', + }, + 'checkpoint.information': { + category: 'checkpoint', + description: 'Policy installation status for a specific blade. ', + name: 'checkpoint.information', + type: 'keyword', + }, + 'checkpoint.layer_name': { + category: 'checkpoint', + description: 'Layer name. ', + name: 'checkpoint.layer_name', + type: 'keyword', + }, + 'checkpoint.layer_uuid': { + category: 'checkpoint', + description: 'Layer UUID. ', + name: 'checkpoint.layer_uuid', + type: 'keyword', + }, + 'checkpoint.log_id': { + category: 'checkpoint', + description: 'Unique identity for logs. ', + name: 'checkpoint.log_id', + type: 'integer', + }, + 'checkpoint.origin_sic_name': { + category: 'checkpoint', + description: 'Machine SIC. ', + name: 'checkpoint.origin_sic_name', + type: 'keyword', + }, + 'checkpoint.policy_mgmt': { + category: 'checkpoint', + description: 'Name of the Management Server that manages this Security Gateway. ', + name: 'checkpoint.policy_mgmt', + type: 'keyword', + }, + 'checkpoint.policy_name': { + category: 'checkpoint', + description: 'Name of the last policy that this Security Gateway fetched. ', + name: 'checkpoint.policy_name', + type: 'keyword', + }, + 'checkpoint.protocol': { + category: 'checkpoint', + description: 'Protocol detected on the connection. ', + name: 'checkpoint.protocol', + type: 'keyword', + }, + 'checkpoint.proxy_src_ip': { + category: 'checkpoint', + description: 'Sender source IP (even when using proxy). ', + name: 'checkpoint.proxy_src_ip', + type: 'ip', + }, + 'checkpoint.rule': { + category: 'checkpoint', + description: 'Matched rule number. ', + name: 'checkpoint.rule', + type: 'integer', + }, + 'checkpoint.rule_action': { + category: 'checkpoint', + description: 'Action of the matched rule in the access policy. ', + name: 'checkpoint.rule_action', + type: 'keyword', + }, + 'checkpoint.scan_direction': { + category: 'checkpoint', + description: 'Scan direction. ', + name: 'checkpoint.scan_direction', + type: 'keyword', + }, + 'checkpoint.session_id': { + category: 'checkpoint', + description: 'Log uuid. ', + name: 'checkpoint.session_id', + type: 'keyword', + }, + 'checkpoint.source_os': { + category: 'checkpoint', + description: 'OS which generated the attack. ', + name: 'checkpoint.source_os', + type: 'keyword', + }, + 'checkpoint.src_country': { + category: 'checkpoint', + description: 'Country name, derived from connection source IP address. ', + name: 'checkpoint.src_country', + type: 'keyword', + }, + 'checkpoint.src_user_name': { + category: 'checkpoint', + description: 'User name connected to source IP ', + name: 'checkpoint.src_user_name', + type: 'keyword', + }, + 'checkpoint.ticket_id': { + category: 'checkpoint', + description: 'Unique ID per file. ', + name: 'checkpoint.ticket_id', + type: 'keyword', + }, + 'checkpoint.tls_server_host_name': { + category: 'checkpoint', + description: 'SNI/CN from encrypted TLS connection used by URLF for categorization. ', + name: 'checkpoint.tls_server_host_name', + type: 'keyword', + }, + 'checkpoint.verdict': { + category: 'checkpoint', + description: 'TE engine verdict Possible values: Malicious/Benign/Error. ', + name: 'checkpoint.verdict', + type: 'keyword', + }, + 'checkpoint.user': { + category: 'checkpoint', + description: 'Source user name. ', + name: 'checkpoint.user', + type: 'keyword', + }, + 'checkpoint.vendor_list': { + category: 'checkpoint', + description: 'The vendor name that provided the verdict for a malicious URL. ', + name: 'checkpoint.vendor_list', + type: 'keyword', + }, + 'checkpoint.web_server_type': { + category: 'checkpoint', + description: 'Web server detected in the HTTP response. ', + name: 'checkpoint.web_server_type', + type: 'keyword', + }, + 'checkpoint.client_name': { + category: 'checkpoint', + description: 'Client Application or Software Blade that detected the event. ', + name: 'checkpoint.client_name', + type: 'keyword', + }, + 'checkpoint.client_version': { + category: 'checkpoint', + description: 'Build version of SandBlast Agent client installed on the computer. ', + name: 'checkpoint.client_version', + type: 'keyword', + }, + 'checkpoint.extension_version': { + category: 'checkpoint', + description: 'Build version of the SandBlast Agent browser extension. ', + name: 'checkpoint.extension_version', + type: 'keyword', + }, + 'checkpoint.host_time': { + category: 'checkpoint', + description: 'Local time on the endpoint computer. ', + name: 'checkpoint.host_time', + type: 'keyword', + }, + 'checkpoint.installed_products': { + category: 'checkpoint', + description: 'List of installed Endpoint Software Blades. ', + name: 'checkpoint.installed_products', + type: 'keyword', + }, + 'checkpoint.cc': { + category: 'checkpoint', + description: 'The Carbon Copy address of the email. ', + name: 'checkpoint.cc', + type: 'keyword', + }, + 'checkpoint.parent_process_username': { + category: 'checkpoint', + description: 'Owner username of the parent process of the process that triggered the attack. ', + name: 'checkpoint.parent_process_username', + type: 'keyword', + }, + 'checkpoint.process_username': { + category: 'checkpoint', + description: 'Owner username of the process that triggered the attack. ', + name: 'checkpoint.process_username', + type: 'keyword', + }, + 'checkpoint.audit_status': { + category: 'checkpoint', + description: 'Audit Status. Can be Success or Failure. ', + name: 'checkpoint.audit_status', + type: 'keyword', + }, + 'checkpoint.objecttable': { + category: 'checkpoint', + description: 'Table of affected objects. ', + name: 'checkpoint.objecttable', + type: 'keyword', + }, + 'checkpoint.objecttype': { + category: 'checkpoint', + description: 'The type of the affected object. ', + name: 'checkpoint.objecttype', + type: 'keyword', + }, + 'checkpoint.operation_number': { + category: 'checkpoint', + description: 'The operation nuber. ', + name: 'checkpoint.operation_number', + type: 'keyword', + }, + 'checkpoint.suppressed_logs': { + category: 'checkpoint', + description: + 'Aggregated connections for five minutes on the same source, destination and port. ', + name: 'checkpoint.suppressed_logs', + type: 'integer', + }, + 'checkpoint.blade_name': { + category: 'checkpoint', + description: 'Blade name. ', + name: 'checkpoint.blade_name', + type: 'keyword', + }, + 'checkpoint.status': { + category: 'checkpoint', + description: 'Ok/Warning/Error. ', + name: 'checkpoint.status', + type: 'keyword', + }, + 'checkpoint.short_desc': { + category: 'checkpoint', + description: 'Short description of the process that was executed. ', + name: 'checkpoint.short_desc', + type: 'keyword', + }, + 'checkpoint.long_desc': { + category: 'checkpoint', + description: 'More information on the process (usually describing error reason in failure). ', + name: 'checkpoint.long_desc', + type: 'keyword', + }, + 'checkpoint.scan_hosts_hour': { + category: 'checkpoint', + description: 'Number of unique hosts during the last hour. ', + name: 'checkpoint.scan_hosts_hour', + type: 'integer', + }, + 'checkpoint.scan_hosts_day': { + category: 'checkpoint', + description: 'Number of unique hosts during the last day. ', + name: 'checkpoint.scan_hosts_day', + type: 'integer', + }, + 'checkpoint.scan_hosts_week': { + category: 'checkpoint', + description: 'Number of unique hosts during the last week. ', + name: 'checkpoint.scan_hosts_week', + type: 'integer', + }, + 'checkpoint.unique_detected_hour': { + category: 'checkpoint', + description: 'Detected virus for a specific host during the last hour. ', + name: 'checkpoint.unique_detected_hour', + type: 'integer', + }, + 'checkpoint.unique_detected_day': { + category: 'checkpoint', + description: 'Detected virus for a specific host during the last day. ', + name: 'checkpoint.unique_detected_day', + type: 'integer', + }, + 'checkpoint.unique_detected_week': { + category: 'checkpoint', + description: 'Detected virus for a specific host during the last week. ', + name: 'checkpoint.unique_detected_week', + type: 'integer', + }, + 'checkpoint.scan_mail': { + category: 'checkpoint', + description: 'Number of emails that were scanned by "AB malicious activity" engine. ', + name: 'checkpoint.scan_mail', + type: 'integer', + }, + 'checkpoint.additional_ip': { + category: 'checkpoint', + description: 'DNS host name. ', + name: 'checkpoint.additional_ip', + type: 'keyword', + }, + 'checkpoint.description': { + category: 'checkpoint', + description: 'Additional explanation how the security gateway enforced the connection. ', + name: 'checkpoint.description', + type: 'keyword', + }, + 'checkpoint.email_spam_category': { + category: 'checkpoint', + description: 'Email categories. Possible values: spam/not spam/phishing. ', + name: 'checkpoint.email_spam_category', + type: 'keyword', + }, + 'checkpoint.email_control_analysis': { + category: 'checkpoint', + description: 'Message classification, received from spam vendor engine. ', + name: 'checkpoint.email_control_analysis', + type: 'keyword', + }, + 'checkpoint.scan_results': { + category: 'checkpoint', + description: '"Infected"/description of a failure. ', + name: 'checkpoint.scan_results', + type: 'keyword', + }, + 'checkpoint.original_queue_id': { + category: 'checkpoint', + description: 'Original postfix email queue id. ', + name: 'checkpoint.original_queue_id', + type: 'keyword', + }, + 'checkpoint.risk': { + category: 'checkpoint', + description: 'Risk level we got from the engine. ', + name: 'checkpoint.risk', + type: 'keyword', + }, + 'checkpoint.observable_name': { + category: 'checkpoint', + description: 'IOC observable signature name. ', + name: 'checkpoint.observable_name', + type: 'keyword', + }, + 'checkpoint.observable_id': { + category: 'checkpoint', + description: 'IOC observable signature id. ', + name: 'checkpoint.observable_id', + type: 'keyword', + }, + 'checkpoint.observable_comment': { + category: 'checkpoint', + description: 'IOC observable signature description. ', + name: 'checkpoint.observable_comment', + type: 'keyword', + }, + 'checkpoint.indicator_name': { + category: 'checkpoint', + description: 'IOC indicator name. ', + name: 'checkpoint.indicator_name', + type: 'keyword', + }, + 'checkpoint.indicator_description': { + category: 'checkpoint', + description: 'IOC indicator description. ', + name: 'checkpoint.indicator_description', + type: 'keyword', + }, + 'checkpoint.indicator_reference': { + category: 'checkpoint', + description: 'IOC indicator reference. ', + name: 'checkpoint.indicator_reference', + type: 'keyword', + }, + 'checkpoint.indicator_uuid': { + category: 'checkpoint', + description: 'IOC indicator uuid. ', + name: 'checkpoint.indicator_uuid', + type: 'keyword', + }, + 'checkpoint.app_desc': { + category: 'checkpoint', + description: 'Application description. ', + name: 'checkpoint.app_desc', + type: 'keyword', + }, + 'checkpoint.app_id': { + category: 'checkpoint', + description: 'Application ID. ', + name: 'checkpoint.app_id', + type: 'integer', + }, + 'checkpoint.certificate_resource': { + category: 'checkpoint', + description: 'HTTPS resource Possible values: SNI or domain name (DN). ', + name: 'checkpoint.certificate_resource', + type: 'keyword', + }, + 'checkpoint.certificate_validation': { + category: 'checkpoint', + description: + 'Precise error, describing HTTPS certificate failure under "HTTPS categorize websites" feature. ', + name: 'checkpoint.certificate_validation', + type: 'keyword', + }, + 'checkpoint.browse_time': { + category: 'checkpoint', + description: 'Application session browse time. ', + name: 'checkpoint.browse_time', + type: 'keyword', + }, + 'checkpoint.limit_requested': { + category: 'checkpoint', + description: 'Indicates whether data limit was requested for the session. ', + name: 'checkpoint.limit_requested', + type: 'integer', + }, + 'checkpoint.limit_applied': { + category: 'checkpoint', + description: 'Indicates whether the session was actually date limited. ', + name: 'checkpoint.limit_applied', + type: 'integer', + }, + 'checkpoint.dropped_total': { + category: 'checkpoint', + description: 'Amount of dropped packets (both incoming and outgoing). ', + name: 'checkpoint.dropped_total', + type: 'integer', + }, + 'checkpoint.client_type_os': { + category: 'checkpoint', + description: 'Client OS detected in the HTTP request. ', + name: 'checkpoint.client_type_os', + type: 'keyword', + }, + 'checkpoint.name': { + category: 'checkpoint', + description: 'Application name. ', + name: 'checkpoint.name', + type: 'keyword', + }, + 'checkpoint.properties': { + category: 'checkpoint', + description: 'Application categories. ', + name: 'checkpoint.properties', + type: 'keyword', + }, + 'checkpoint.sig_id': { + category: 'checkpoint', + description: "Application's signature ID which how it was detected by. ", + name: 'checkpoint.sig_id', + type: 'keyword', + }, + 'checkpoint.desc': { + category: 'checkpoint', + description: 'Override application description. ', + name: 'checkpoint.desc', + type: 'keyword', + }, + 'checkpoint.referrer_self_uid': { + category: 'checkpoint', + description: 'UUID of the current log. ', + name: 'checkpoint.referrer_self_uid', + type: 'keyword', + }, + 'checkpoint.referrer_parent_uid': { + category: 'checkpoint', + description: 'Log UUID of the referring application. ', + name: 'checkpoint.referrer_parent_uid', + type: 'keyword', + }, + 'checkpoint.needs_browse_time': { + category: 'checkpoint', + description: 'Browse time required for the connection. ', + name: 'checkpoint.needs_browse_time', + type: 'integer', + }, + 'checkpoint.cluster_info': { + category: 'checkpoint', + description: + 'Cluster information. Possible options: Failover reason/cluster state changes/CP cluster or 3rd party. ', + name: 'checkpoint.cluster_info', + type: 'keyword', + }, + 'checkpoint.sync': { + category: 'checkpoint', + description: 'Sync status and the reason (stable, at risk). ', + name: 'checkpoint.sync', + type: 'keyword', + }, + 'checkpoint.file_direction': { + category: 'checkpoint', + description: 'File direction. Possible options: upload/download. ', + name: 'checkpoint.file_direction', + type: 'keyword', + }, + 'checkpoint.invalid_file_size': { + category: 'checkpoint', + description: 'File_size field is valid only if this field is set to 0. ', + name: 'checkpoint.invalid_file_size', + type: 'integer', + }, + 'checkpoint.top_archive_file_name': { + category: 'checkpoint', + description: 'In case of archive file: the file that was sent/received. ', + name: 'checkpoint.top_archive_file_name', + type: 'keyword', + }, + 'checkpoint.data_type_name': { + category: 'checkpoint', + description: 'Data type in rulebase that was matched. ', + name: 'checkpoint.data_type_name', + type: 'keyword', + }, + 'checkpoint.specific_data_type_name': { + category: 'checkpoint', + description: 'Compound/Group scenario, data type that was matched. ', + name: 'checkpoint.specific_data_type_name', + type: 'keyword', + }, + 'checkpoint.word_list': { + category: 'checkpoint', + description: 'Words matched by data type. ', + name: 'checkpoint.word_list', + type: 'keyword', + }, + 'checkpoint.info': { + category: 'checkpoint', + description: 'Special log message. ', + name: 'checkpoint.info', + type: 'keyword', + }, + 'checkpoint.outgoing_url': { + category: 'checkpoint', + description: 'URL related to this log (for HTTP). ', + name: 'checkpoint.outgoing_url', + type: 'keyword', + }, + 'checkpoint.dlp_rule_name': { + category: 'checkpoint', + description: 'Matched rule name. ', + name: 'checkpoint.dlp_rule_name', + type: 'keyword', + }, + 'checkpoint.dlp_recipients': { + category: 'checkpoint', + description: 'Mail recipients. ', + name: 'checkpoint.dlp_recipients', + type: 'keyword', + }, + 'checkpoint.dlp_subject': { + category: 'checkpoint', + description: 'Mail subject. ', + name: 'checkpoint.dlp_subject', + type: 'keyword', + }, + 'checkpoint.dlp_word_list': { + category: 'checkpoint', + description: 'Phrases matched by data type. ', + name: 'checkpoint.dlp_word_list', + type: 'keyword', + }, + 'checkpoint.dlp_template_score': { + category: 'checkpoint', + description: 'Template data type match score. ', + name: 'checkpoint.dlp_template_score', + type: 'keyword', + }, + 'checkpoint.message_size': { + category: 'checkpoint', + description: 'Mail/post size. ', + name: 'checkpoint.message_size', + type: 'integer', + }, + 'checkpoint.dlp_incident_uid': { + category: 'checkpoint', + description: 'Unique ID of the matched rule. ', + name: 'checkpoint.dlp_incident_uid', + type: 'keyword', + }, + 'checkpoint.dlp_related_incident_uid': { + category: 'checkpoint', + description: 'Other ID related to this one. ', + name: 'checkpoint.dlp_related_incident_uid', + type: 'keyword', + }, + 'checkpoint.dlp_data_type_name': { + category: 'checkpoint', + description: 'Matched data type. ', + name: 'checkpoint.dlp_data_type_name', + type: 'keyword', + }, + 'checkpoint.dlp_data_type_uid': { + category: 'checkpoint', + description: 'Unique ID of the matched data type. ', + name: 'checkpoint.dlp_data_type_uid', + type: 'keyword', + }, + 'checkpoint.dlp_violation_description': { + category: 'checkpoint', + description: 'Violation descriptions described in the rulebase. ', + name: 'checkpoint.dlp_violation_description', + type: 'keyword', + }, + 'checkpoint.dlp_relevant_data_types': { + category: 'checkpoint', + description: 'In case of Compound/Group: the inner data types that were matched. ', + name: 'checkpoint.dlp_relevant_data_types', + type: 'keyword', + }, + 'checkpoint.dlp_action_reason': { + category: 'checkpoint', + description: 'Action chosen reason. ', + name: 'checkpoint.dlp_action_reason', + type: 'keyword', + }, + 'checkpoint.dlp_categories': { + category: 'checkpoint', + description: 'Data type category. ', + name: 'checkpoint.dlp_categories', + type: 'keyword', + }, + 'checkpoint.dlp_transint': { + category: 'checkpoint', + description: 'HTTP/SMTP/FTP. ', + name: 'checkpoint.dlp_transint', + type: 'keyword', + }, + 'checkpoint.duplicate': { + category: 'checkpoint', + description: + 'Log marked as duplicated, when mail is split and the Security Gateway sees it twice. ', + name: 'checkpoint.duplicate', + type: 'keyword', + }, + 'checkpoint.matched_file': { + category: 'checkpoint', + description: 'Unique ID of the matched data type. ', + name: 'checkpoint.matched_file', + type: 'keyword', + }, + 'checkpoint.matched_file_text_segments': { + category: 'checkpoint', + description: 'Fingerprint: number of text segments matched by this traffic. ', + name: 'checkpoint.matched_file_text_segments', + type: 'integer', + }, + 'checkpoint.matched_file_percentage': { + category: 'checkpoint', + description: 'Fingerprint: match percentage of the traffic. ', + name: 'checkpoint.matched_file_percentage', + type: 'integer', + }, + 'checkpoint.dlp_additional_action': { + category: 'checkpoint', + description: 'Watermark/None. ', + name: 'checkpoint.dlp_additional_action', + type: 'keyword', + }, + 'checkpoint.dlp_watermark_profile': { + category: 'checkpoint', + description: 'Watermark which was applied. ', + name: 'checkpoint.dlp_watermark_profile', + type: 'keyword', + }, + 'checkpoint.dlp_repository_id': { + category: 'checkpoint', + description: 'ID of scanned repository. ', + name: 'checkpoint.dlp_repository_id', + type: 'keyword', + }, + 'checkpoint.dlp_repository_root_path': { + category: 'checkpoint', + description: 'Repository path. ', + name: 'checkpoint.dlp_repository_root_path', + type: 'keyword', + }, + 'checkpoint.scan_id': { + category: 'checkpoint', + description: 'Sequential number of scan. ', + name: 'checkpoint.scan_id', + type: 'keyword', + }, + 'checkpoint.special_properties': { + category: 'checkpoint', + description: + "If this field is set to '1' the log will not be shown (in use for monitoring scan progress). ", + name: 'checkpoint.special_properties', + type: 'integer', + }, + 'checkpoint.dlp_repository_total_size': { + category: 'checkpoint', + description: 'Repository size. ', + name: 'checkpoint.dlp_repository_total_size', + type: 'integer', + }, + 'checkpoint.dlp_repository_files_number': { + category: 'checkpoint', + description: 'Number of files in repository. ', + name: 'checkpoint.dlp_repository_files_number', + type: 'integer', + }, + 'checkpoint.dlp_repository_scanned_files_number': { + category: 'checkpoint', + description: 'Number of scanned files in repository. ', + name: 'checkpoint.dlp_repository_scanned_files_number', + type: 'integer', + }, + 'checkpoint.duration': { + category: 'checkpoint', + description: 'Scan duration. ', + name: 'checkpoint.duration', + type: 'keyword', + }, + 'checkpoint.dlp_fingerprint_long_status': { + category: 'checkpoint', + description: 'Scan status - long format. ', + name: 'checkpoint.dlp_fingerprint_long_status', + type: 'keyword', + }, + 'checkpoint.dlp_fingerprint_short_status': { + category: 'checkpoint', + description: 'Scan status - short format. ', + name: 'checkpoint.dlp_fingerprint_short_status', + type: 'keyword', + }, + 'checkpoint.dlp_repository_directories_number': { + category: 'checkpoint', + description: 'Number of directories in repository. ', + name: 'checkpoint.dlp_repository_directories_number', + type: 'integer', + }, + 'checkpoint.dlp_repository_unreachable_directories_number': { + category: 'checkpoint', + description: 'Number of directories the Security Gateway was unable to read. ', + name: 'checkpoint.dlp_repository_unreachable_directories_number', + type: 'integer', + }, + 'checkpoint.dlp_fingerprint_files_number': { + category: 'checkpoint', + description: 'Number of successfully scanned files in repository. ', + name: 'checkpoint.dlp_fingerprint_files_number', + type: 'integer', + }, + 'checkpoint.dlp_repository_skipped_files_number': { + category: 'checkpoint', + description: 'Skipped number of files because of configuration. ', + name: 'checkpoint.dlp_repository_skipped_files_number', + type: 'integer', + }, + 'checkpoint.dlp_repository_scanned_directories_number': { + category: 'checkpoint', + description: 'Amount of directories scanned. ', + name: 'checkpoint.dlp_repository_scanned_directories_number', + type: 'integer', + }, + 'checkpoint.number_of_errors': { + category: 'checkpoint', + description: 'Number of files that were not scanned due to an error. ', + name: 'checkpoint.number_of_errors', + type: 'integer', + }, + 'checkpoint.next_scheduled_scan_date': { + category: 'checkpoint', + description: 'Next scan scheduled time according to time object. ', + name: 'checkpoint.next_scheduled_scan_date', + type: 'keyword', + }, + 'checkpoint.dlp_repository_scanned_total_size': { + category: 'checkpoint', + description: 'Size scanned. ', + name: 'checkpoint.dlp_repository_scanned_total_size', + type: 'integer', + }, + 'checkpoint.dlp_repository_reached_directories_number': { + category: 'checkpoint', + description: 'Number of scanned directories in repository. ', + name: 'checkpoint.dlp_repository_reached_directories_number', + type: 'integer', + }, + 'checkpoint.dlp_repository_not_scanned_directories_percentage': { + category: 'checkpoint', + description: 'Percentage of directories the Security Gateway was unable to read. ', + name: 'checkpoint.dlp_repository_not_scanned_directories_percentage', + type: 'integer', + }, + 'checkpoint.speed': { + category: 'checkpoint', + description: 'Current scan speed. ', + name: 'checkpoint.speed', + type: 'integer', + }, + 'checkpoint.dlp_repository_scan_progress': { + category: 'checkpoint', + description: 'Scan percentage. ', + name: 'checkpoint.dlp_repository_scan_progress', + type: 'integer', + }, + 'checkpoint.sub_policy_name': { + category: 'checkpoint', + description: 'Layer name. ', + name: 'checkpoint.sub_policy_name', + type: 'keyword', + }, + 'checkpoint.sub_policy_uid': { + category: 'checkpoint', + description: 'Layer uid. ', + name: 'checkpoint.sub_policy_uid', + type: 'keyword', + }, + 'checkpoint.fw_message': { + category: 'checkpoint', + description: 'Used for various firewall errors. ', + name: 'checkpoint.fw_message', + type: 'keyword', + }, + 'checkpoint.message': { + category: 'checkpoint', + description: 'ISP link has failed. ', + name: 'checkpoint.message', + type: 'keyword', + }, + 'checkpoint.isp_link': { + category: 'checkpoint', + description: 'Name of ISP link. ', + name: 'checkpoint.isp_link', + type: 'keyword', + }, + 'checkpoint.fw_subproduct': { + category: 'checkpoint', + description: 'Can be vpn/non vpn. ', + name: 'checkpoint.fw_subproduct', + type: 'keyword', + }, + 'checkpoint.sctp_error': { + category: 'checkpoint', + description: 'Error information, what caused sctp to fail on out_of_state. ', + name: 'checkpoint.sctp_error', + type: 'keyword', + }, + 'checkpoint.chunk_type': { + category: 'checkpoint', + description: 'Chunck of the sctp stream. ', + name: 'checkpoint.chunk_type', + type: 'keyword', + }, + 'checkpoint.sctp_association_state': { + category: 'checkpoint', + description: 'The bad state you were trying to update to. ', + name: 'checkpoint.sctp_association_state', + type: 'keyword', + }, + 'checkpoint.tcp_packet_out_of_state': { + category: 'checkpoint', + description: 'State violation. ', + name: 'checkpoint.tcp_packet_out_of_state', + type: 'keyword', + }, + 'checkpoint.connectivity_level': { + category: 'checkpoint', + description: 'Log for a new connection in wire mode. ', + name: 'checkpoint.connectivity_level', + type: 'keyword', + }, + 'checkpoint.ip_option': { + category: 'checkpoint', + description: 'IP option that was dropped. ', + name: 'checkpoint.ip_option', + type: 'integer', + }, + 'checkpoint.tcp_state': { + category: 'checkpoint', + description: 'Log reinting a tcp state change. ', + name: 'checkpoint.tcp_state', + type: 'keyword', + }, + 'checkpoint.expire_time': { + category: 'checkpoint', + description: 'Connection closing time. ', + name: 'checkpoint.expire_time', + type: 'keyword', + }, + 'checkpoint.rpc_prog': { + category: 'checkpoint', + description: 'Log for new RPC state - prog values. ', + name: 'checkpoint.rpc_prog', + type: 'integer', + }, + 'checkpoint.dce-rpc_interface_uuid': { + category: 'checkpoint', + description: 'Log for new RPC state - UUID values ', + name: 'checkpoint.dce-rpc_interface_uuid', + type: 'keyword', + }, + 'checkpoint.elapsed': { + category: 'checkpoint', + description: 'Time passed since start time. ', + name: 'checkpoint.elapsed', + type: 'keyword', + }, + 'checkpoint.icmp': { + category: 'checkpoint', + description: 'Number of packets, received by the client. ', + name: 'checkpoint.icmp', + type: 'keyword', + }, + 'checkpoint.capture_uuid': { + category: 'checkpoint', + description: 'UUID generated for the capture. Used when enabling the capture when logging. ', + name: 'checkpoint.capture_uuid', + type: 'keyword', + }, + 'checkpoint.diameter_app_ID': { + category: 'checkpoint', + description: 'The ID of diameter application. ', + name: 'checkpoint.diameter_app_ID', + type: 'integer', + }, + 'checkpoint.diameter_cmd_code': { + category: 'checkpoint', + description: 'Diameter not allowed application command id. ', + name: 'checkpoint.diameter_cmd_code', + type: 'integer', + }, + 'checkpoint.diameter_msg_type': { + category: 'checkpoint', + description: 'Diameter message type. ', + name: 'checkpoint.diameter_msg_type', + type: 'keyword', + }, + 'checkpoint.cp_message': { + category: 'checkpoint', + description: 'Used to log a general message. ', + name: 'checkpoint.cp_message', + type: 'integer', + }, + 'checkpoint.log_delay': { + category: 'checkpoint', + description: 'Time left before deleting template. ', + name: 'checkpoint.log_delay', + type: 'integer', + }, + 'checkpoint.attack_status': { + category: 'checkpoint', + description: 'In case of a malicious event on an endpoint computer, the status of the attack. ', + name: 'checkpoint.attack_status', + type: 'keyword', + }, + 'checkpoint.impacted_files': { + category: 'checkpoint', + description: + 'In case of an infection on an endpoint computer, the list of files that the malware impacted. ', + name: 'checkpoint.impacted_files', + type: 'keyword', + }, + 'checkpoint.remediated_files': { + category: 'checkpoint', + description: + 'In case of an infection and a successful cleaning of that infection, this is a list of remediated files on the computer. ', + name: 'checkpoint.remediated_files', + type: 'keyword', + }, + 'checkpoint.triggered_by': { + category: 'checkpoint', + description: + 'The name of the mechanism that triggered the Software Blade to enforce a protection. ', + name: 'checkpoint.triggered_by', + type: 'keyword', + }, + 'checkpoint.https_inspection_rule_id': { + category: 'checkpoint', + description: 'ID of the matched rule. ', + name: 'checkpoint.https_inspection_rule_id', + type: 'keyword', + }, + 'checkpoint.https_inspection_rule_name': { + category: 'checkpoint', + description: 'Name of the matched rule. ', + name: 'checkpoint.https_inspection_rule_name', + type: 'keyword', + }, + 'checkpoint.app_properties': { + category: 'checkpoint', + description: 'List of all found categories. ', + name: 'checkpoint.app_properties', + type: 'keyword', + }, + 'checkpoint.https_validation': { + category: 'checkpoint', + description: 'Precise error, describing HTTPS inspection failure. ', + name: 'checkpoint.https_validation', + type: 'keyword', + }, + 'checkpoint.https_inspection_action': { + category: 'checkpoint', + description: 'HTTPS inspection action (Inspect/Bypass/Error). ', + name: 'checkpoint.https_inspection_action', + type: 'keyword', + }, + 'checkpoint.icap_service_id': { + category: 'checkpoint', + description: 'Service ID, can work with multiple servers, treated as services. ', + name: 'checkpoint.icap_service_id', + type: 'integer', + }, + 'checkpoint.icap_server_name': { + category: 'checkpoint', + description: 'Server name. ', + name: 'checkpoint.icap_server_name', + type: 'keyword', + }, + 'checkpoint.internal_error': { + category: 'checkpoint', + description: 'Internal error, for troubleshooting ', + name: 'checkpoint.internal_error', + type: 'keyword', + }, + 'checkpoint.icap_more_info': { + category: 'checkpoint', + description: 'Free text for verdict. ', + name: 'checkpoint.icap_more_info', + type: 'integer', + }, + 'checkpoint.reply_status': { + category: 'checkpoint', + description: 'ICAP reply status code, e.g. 200 or 204. ', + name: 'checkpoint.reply_status', + type: 'integer', + }, + 'checkpoint.icap_server_service': { + category: 'checkpoint', + description: 'Service name, as given in the ICAP URI ', + name: 'checkpoint.icap_server_service', + type: 'keyword', + }, + 'checkpoint.mirror_and_decrypt_type': { + category: 'checkpoint', + description: + 'Information about decrypt and forward. Possible values: Mirror only, Decrypt and mirror, Partial mirroring (HTTPS inspection Bypass). ', + name: 'checkpoint.mirror_and_decrypt_type', + type: 'keyword', + }, + 'checkpoint.interface_name': { + category: 'checkpoint', + description: 'Designated interface for mirror And decrypt. ', + name: 'checkpoint.interface_name', + type: 'keyword', + }, + 'checkpoint.session_uid': { + category: 'checkpoint', + description: 'HTTP session-id. ', + name: 'checkpoint.session_uid', + type: 'keyword', + }, + 'checkpoint.broker_publisher': { + category: 'checkpoint', + description: 'IP address of the broker publisher who shared the session information. ', + name: 'checkpoint.broker_publisher', + type: 'ip', + }, + 'checkpoint.src_user_dn': { + category: 'checkpoint', + description: 'User distinguished name connected to source IP. ', + name: 'checkpoint.src_user_dn', + type: 'keyword', + }, + 'checkpoint.proxy_user_name': { + category: 'checkpoint', + description: 'User name connected to proxy IP. ', + name: 'checkpoint.proxy_user_name', + type: 'keyword', + }, + 'checkpoint.proxy_machine_name': { + category: 'checkpoint', + description: 'Machine name connected to proxy IP. ', + name: 'checkpoint.proxy_machine_name', + type: 'integer', + }, + 'checkpoint.proxy_user_dn': { + category: 'checkpoint', + description: 'User distinguished name connected to proxy IP. ', + name: 'checkpoint.proxy_user_dn', + type: 'keyword', + }, + 'checkpoint.query': { + category: 'checkpoint', + description: 'DNS query. ', + name: 'checkpoint.query', + type: 'keyword', + }, + 'checkpoint.dns_query': { + category: 'checkpoint', + description: 'DNS query. ', + name: 'checkpoint.dns_query', + type: 'keyword', + }, + 'checkpoint.inspection_item': { + category: 'checkpoint', + description: 'Blade element performed inspection. ', + name: 'checkpoint.inspection_item', + type: 'keyword', + }, + 'checkpoint.inspection_category': { + category: 'checkpoint', + description: 'Inspection category: protocol anomaly, signature etc. ', + name: 'checkpoint.inspection_category', + type: 'keyword', + }, + 'checkpoint.inspection_profile': { + category: 'checkpoint', + description: 'Profile which the activated protection belongs to. ', + name: 'checkpoint.inspection_profile', + type: 'keyword', + }, + 'checkpoint.summary': { + category: 'checkpoint', + description: 'Summary message of a non-compliant DNS traffic drops or detects. ', + name: 'checkpoint.summary', + type: 'keyword', + }, + 'checkpoint.question_rdata': { + category: 'checkpoint', + description: 'List of question records domains. ', + name: 'checkpoint.question_rdata', + type: 'keyword', + }, + 'checkpoint.answer_rdata': { + category: 'checkpoint', + description: 'List of answer resource records to the questioned domains. ', + name: 'checkpoint.answer_rdata', + type: 'keyword', + }, + 'checkpoint.authority_rdata': { + category: 'checkpoint', + description: 'List of authoritative servers. ', + name: 'checkpoint.authority_rdata', + type: 'keyword', + }, + 'checkpoint.additional_rdata': { + category: 'checkpoint', + description: 'List of additional resource records. ', + name: 'checkpoint.additional_rdata', + type: 'keyword', + }, + 'checkpoint.files_names': { + category: 'checkpoint', + description: 'List of files requested by FTP. ', + name: 'checkpoint.files_names', + type: 'keyword', + }, + 'checkpoint.ftp_user': { + category: 'checkpoint', + description: 'FTP username. ', + name: 'checkpoint.ftp_user', + type: 'keyword', + }, + 'checkpoint.mime_from': { + category: 'checkpoint', + description: "Sender's address. ", + name: 'checkpoint.mime_from', + type: 'keyword', + }, + 'checkpoint.mime_to': { + category: 'checkpoint', + description: 'List of receiver address. ', + name: 'checkpoint.mime_to', + type: 'keyword', + }, + 'checkpoint.bcc': { + category: 'checkpoint', + description: 'List of BCC addresses. ', + name: 'checkpoint.bcc', + type: 'keyword', + }, + 'checkpoint.content_type': { + category: 'checkpoint', + description: + 'Mail content type. Possible values: application/msword, text/html, image/gif etc. ', + name: 'checkpoint.content_type', + type: 'keyword', + }, + 'checkpoint.user_agent': { + category: 'checkpoint', + description: 'String identifying requesting software user agent. ', + name: 'checkpoint.user_agent', + type: 'keyword', + }, + 'checkpoint.referrer': { + category: 'checkpoint', + description: 'Referrer HTTP request header, previous web page address. ', + name: 'checkpoint.referrer', + type: 'keyword', + }, + 'checkpoint.http_location': { + category: 'checkpoint', + description: 'Response header, indicates the URL to redirect a page to. ', + name: 'checkpoint.http_location', + type: 'keyword', + }, + 'checkpoint.content_disposition': { + category: 'checkpoint', + description: 'Indicates how the content is expected to be displayed inline in the browser. ', + name: 'checkpoint.content_disposition', + type: 'keyword', + }, + 'checkpoint.via': { + category: 'checkpoint', + description: + 'Via header is added by proxies for tracking purposes to avoid sending reqests in loop. ', + name: 'checkpoint.via', + type: 'keyword', + }, + 'checkpoint.http_server': { + category: 'checkpoint', + description: + 'Server HTTP header value, contains information about the software used by the origin server, which handles the request. ', + name: 'checkpoint.http_server', + type: 'keyword', + }, + 'checkpoint.content_length': { + category: 'checkpoint', + description: 'Indicates the size of the entity-body of the HTTP header. ', + name: 'checkpoint.content_length', + type: 'keyword', + }, + 'checkpoint.authorization': { + category: 'checkpoint', + description: 'Authorization HTTP header value. ', + name: 'checkpoint.authorization', + type: 'keyword', + }, + 'checkpoint.http_host': { + category: 'checkpoint', + description: 'Domain name of the server that the HTTP request is sent to. ', + name: 'checkpoint.http_host', + type: 'keyword', + }, + 'checkpoint.inspection_settings_log': { + category: 'checkpoint', + description: 'Indicats that the log was released by inspection settings. ', + name: 'checkpoint.inspection_settings_log', + type: 'keyword', + }, + 'checkpoint.cvpn_resource': { + category: 'checkpoint', + description: 'Mobile Access application. ', + name: 'checkpoint.cvpn_resource', + type: 'keyword', + }, + 'checkpoint.cvpn_category': { + category: 'checkpoint', + description: 'Mobile Access application type. ', + name: 'checkpoint.cvpn_category', + type: 'keyword', + }, + 'checkpoint.url': { + category: 'checkpoint', + description: 'Translated URL. ', + name: 'checkpoint.url', + type: 'keyword', + }, + 'checkpoint.reject_id': { + category: 'checkpoint', + description: + 'A reject ID that corresponds to the one presented in the Mobile Access error page. ', + name: 'checkpoint.reject_id', + type: 'keyword', + }, + 'checkpoint.fs-proto': { + category: 'checkpoint', + description: 'The file share protocol used in mobile acess file share application. ', + name: 'checkpoint.fs-proto', + type: 'keyword', + }, + 'checkpoint.app_package': { + category: 'checkpoint', + description: 'Unique identifier of the application on the protected mobile device. ', + name: 'checkpoint.app_package', + type: 'keyword', + }, + 'checkpoint.appi_name': { + category: 'checkpoint', + description: 'Name of application downloaded on the protected mobile device. ', + name: 'checkpoint.appi_name', + type: 'keyword', + }, + 'checkpoint.app_repackaged': { + category: 'checkpoint', + description: + 'Indicates whether the original application was repackage not by the official developer. ', + name: 'checkpoint.app_repackaged', + type: 'keyword', + }, + 'checkpoint.app_sid_id': { + category: 'checkpoint', + description: 'Unique SHA identifier of a mobile application. ', + name: 'checkpoint.app_sid_id', + type: 'keyword', + }, + 'checkpoint.app_version': { + category: 'checkpoint', + description: 'Version of the application downloaded on the protected mobile device. ', + name: 'checkpoint.app_version', + type: 'keyword', + }, + 'checkpoint.developer_certificate_name': { + category: 'checkpoint', + description: + "Name of the developer's certificate that was used to sign the mobile application. ", + name: 'checkpoint.developer_certificate_name', + type: 'keyword', + }, + 'checkpoint.email_message_id': { + category: 'checkpoint', + description: 'Email session id (uniqe ID of the mail). ', + name: 'checkpoint.email_message_id', + type: 'keyword', + }, + 'checkpoint.email_queue_id': { + category: 'checkpoint', + description: 'Postfix email queue id. ', + name: 'checkpoint.email_queue_id', + type: 'keyword', + }, + 'checkpoint.email_queue_name': { + category: 'checkpoint', + description: 'Postfix email queue name. ', + name: 'checkpoint.email_queue_name', + type: 'keyword', + }, + 'checkpoint.file_name': { + category: 'checkpoint', + description: 'Malicious file name. ', + name: 'checkpoint.file_name', + type: 'keyword', + }, + 'checkpoint.failure_reason': { + category: 'checkpoint', + description: 'MTA failure description. ', + name: 'checkpoint.failure_reason', + type: 'keyword', + }, + 'checkpoint.email_headers': { + category: 'checkpoint', + description: 'String containing all the email headers. ', + name: 'checkpoint.email_headers', + type: 'keyword', + }, + 'checkpoint.arrival_time': { + category: 'checkpoint', + description: 'Email arrival timestamp. ', + name: 'checkpoint.arrival_time', + type: 'keyword', + }, + 'checkpoint.email_status': { + category: 'checkpoint', + description: + "Describes the email's state. Possible options: delivered, deferred, skipped, bounced, hold, new, scan_started, scan_ended ", + name: 'checkpoint.email_status', + type: 'keyword', + }, + 'checkpoint.status_update': { + category: 'checkpoint', + description: 'Last time log was updated. ', + name: 'checkpoint.status_update', + type: 'keyword', + }, + 'checkpoint.delivery_time': { + category: 'checkpoint', + description: 'Timestamp of when email was delivered (MTA finished handling the email. ', + name: 'checkpoint.delivery_time', + type: 'keyword', + }, + 'checkpoint.links_num': { + category: 'checkpoint', + description: 'Number of links in the mail. ', + name: 'checkpoint.links_num', + type: 'integer', + }, + 'checkpoint.attachments_num': { + category: 'checkpoint', + description: 'Number of attachments in the mail. ', + name: 'checkpoint.attachments_num', + type: 'integer', + }, + 'checkpoint.email_content': { + category: 'checkpoint', + description: + 'Mail contents. Possible options: attachments/links & attachments/links/text only. ', + name: 'checkpoint.email_content', + type: 'keyword', + }, + 'checkpoint.allocated_ports': { + category: 'checkpoint', + description: 'Amount of allocated ports. ', + name: 'checkpoint.allocated_ports', + type: 'integer', + }, + 'checkpoint.capacity': { + category: 'checkpoint', + description: 'Capacity of the ports. ', + name: 'checkpoint.capacity', + type: 'integer', + }, + 'checkpoint.ports_usage': { + category: 'checkpoint', + description: 'Percentage of allocated ports. ', + name: 'checkpoint.ports_usage', + type: 'integer', + }, + 'checkpoint.nat_exhausted_pool': { + category: 'checkpoint', + description: '4-tuple of an exhausted pool. ', + name: 'checkpoint.nat_exhausted_pool', + type: 'keyword', + }, + 'checkpoint.nat_rulenum': { + category: 'checkpoint', + description: 'NAT rulebase first matched rule. ', + name: 'checkpoint.nat_rulenum', + type: 'integer', + }, + 'checkpoint.nat_addtnl_rulenum': { + category: 'checkpoint', + description: + 'When matching 2 automatic rules , second rule match will be shown otherwise field will be 0. ', + name: 'checkpoint.nat_addtnl_rulenum', + type: 'integer', + }, + 'checkpoint.message_info': { + category: 'checkpoint', + description: 'Used for information messages, for example:NAT connection has ended. ', + name: 'checkpoint.message_info', + type: 'keyword', + }, + 'checkpoint.nat46': { + category: 'checkpoint', + description: 'NAT 46 status, in most cases "enabled". ', + name: 'checkpoint.nat46', + type: 'keyword', + }, + 'checkpoint.end_time': { + category: 'checkpoint', + description: 'TCP connection end time. ', + name: 'checkpoint.end_time', + type: 'keyword', + }, + 'checkpoint.tcp_end_reason': { + category: 'checkpoint', + description: 'Reason for TCP connection closure. ', + name: 'checkpoint.tcp_end_reason', + type: 'keyword', + }, + 'checkpoint.cgnet': { + category: 'checkpoint', + description: 'Describes NAT allocation for specific subscriber. ', + name: 'checkpoint.cgnet', + type: 'keyword', + }, + 'checkpoint.subscriber': { + category: 'checkpoint', + description: 'Source IP before CGNAT. ', + name: 'checkpoint.subscriber', + type: 'ip', + }, + 'checkpoint.hide_ip': { + category: 'checkpoint', + description: 'Source IP which will be used after CGNAT. ', + name: 'checkpoint.hide_ip', + type: 'ip', + }, + 'checkpoint.int_start': { + category: 'checkpoint', + description: 'Subscriber start int which will be used for NAT. ', + name: 'checkpoint.int_start', + type: 'integer', + }, + 'checkpoint.int_end': { + category: 'checkpoint', + description: 'Subscriber end int which will be used for NAT. ', + name: 'checkpoint.int_end', + type: 'integer', + }, + 'checkpoint.packet_amount': { + category: 'checkpoint', + description: 'Amount of packets dropped. ', + name: 'checkpoint.packet_amount', + type: 'integer', + }, + 'checkpoint.monitor_reason': { + category: 'checkpoint', + description: 'Aggregated logs of monitored packets. ', + name: 'checkpoint.monitor_reason', + type: 'keyword', + }, + 'checkpoint.drops_amount': { + category: 'checkpoint', + description: 'Amount of multicast packets dropped. ', + name: 'checkpoint.drops_amount', + type: 'integer', + }, + 'checkpoint.securexl_message': { + category: 'checkpoint', + description: + 'Two options for a SecureXL message: 1. Missed accounting records after heavy load on logging system. 2. FW log message regarding a packet drop. ', + name: 'checkpoint.securexl_message', + type: 'keyword', + }, + 'checkpoint.conns_amount': { + category: 'checkpoint', + description: 'Connections amount of aggregated log info. ', + name: 'checkpoint.conns_amount', + type: 'integer', + }, + 'checkpoint.scope': { + category: 'checkpoint', + description: 'IP related to the attack. ', + name: 'checkpoint.scope', + type: 'keyword', + }, + 'checkpoint.analyzed_on': { + category: 'checkpoint', + description: 'Check Point ThreatCloud / emulator name. ', + name: 'checkpoint.analyzed_on', + type: 'keyword', + }, + 'checkpoint.detected_on': { + category: 'checkpoint', + description: 'System and applications version the file was emulated on. ', + name: 'checkpoint.detected_on', + type: 'keyword', + }, + 'checkpoint.dropped_file_name': { + category: 'checkpoint', + description: 'List of names dropped from the original file. ', + name: 'checkpoint.dropped_file_name', + type: 'keyword', + }, + 'checkpoint.dropped_file_type': { + category: 'checkpoint', + description: 'List of file types dropped from the original file. ', + name: 'checkpoint.dropped_file_type', + type: 'keyword', + }, + 'checkpoint.dropped_file_hash': { + category: 'checkpoint', + description: 'List of file hashes dropped from the original file. ', + name: 'checkpoint.dropped_file_hash', + type: 'keyword', + }, + 'checkpoint.dropped_file_verdict': { + category: 'checkpoint', + description: 'List of file verdics dropped from the original file. ', + name: 'checkpoint.dropped_file_verdict', + type: 'keyword', + }, + 'checkpoint.emulated_on': { + category: 'checkpoint', + description: 'Images the files were emulated on. ', + name: 'checkpoint.emulated_on', + type: 'keyword', + }, + 'checkpoint.extracted_file_type': { + category: 'checkpoint', + description: 'Types of extracted files in case of an archive. ', + name: 'checkpoint.extracted_file_type', + type: 'keyword', + }, + 'checkpoint.extracted_file_names': { + category: 'checkpoint', + description: 'Names of extracted files in case of an archive. ', + name: 'checkpoint.extracted_file_names', + type: 'keyword', + }, + 'checkpoint.extracted_file_hash': { + category: 'checkpoint', + description: 'Archive hash in case of extracted files. ', + name: 'checkpoint.extracted_file_hash', + type: 'keyword', + }, + 'checkpoint.extracted_file_verdict': { + category: 'checkpoint', + description: 'Verdict of extracted files in case of an archive. ', + name: 'checkpoint.extracted_file_verdict', + type: 'keyword', + }, + 'checkpoint.extracted_file_uid': { + category: 'checkpoint', + description: 'UID of extracted files in case of an archive. ', + name: 'checkpoint.extracted_file_uid', + type: 'keyword', + }, + 'checkpoint.mitre_initial_access': { + category: 'checkpoint', + description: 'The adversary is trying to break into your network. ', + name: 'checkpoint.mitre_initial_access', + type: 'keyword', + }, + 'checkpoint.mitre_execution': { + category: 'checkpoint', + description: 'The adversary is trying to run malicious code. ', + name: 'checkpoint.mitre_execution', + type: 'keyword', + }, + 'checkpoint.mitre_persistence': { + category: 'checkpoint', + description: 'The adversary is trying to maintain his foothold. ', + name: 'checkpoint.mitre_persistence', + type: 'keyword', + }, + 'checkpoint.mitre_privilege_escalation': { + category: 'checkpoint', + description: 'The adversary is trying to gain higher-level permissions. ', + name: 'checkpoint.mitre_privilege_escalation', + type: 'keyword', + }, + 'checkpoint.mitre_defense_evasion': { + category: 'checkpoint', + description: 'The adversary is trying to avoid being detected. ', + name: 'checkpoint.mitre_defense_evasion', + type: 'keyword', + }, + 'checkpoint.mitre_credential_access': { + category: 'checkpoint', + description: 'The adversary is trying to steal account names and passwords. ', + name: 'checkpoint.mitre_credential_access', + type: 'keyword', + }, + 'checkpoint.mitre_discovery': { + category: 'checkpoint', + description: 'The adversary is trying to expose information about your environment. ', + name: 'checkpoint.mitre_discovery', + type: 'keyword', + }, + 'checkpoint.mitre_lateral_movement': { + category: 'checkpoint', + description: 'The adversary is trying to explore your environment. ', + name: 'checkpoint.mitre_lateral_movement', + type: 'keyword', + }, + 'checkpoint.mitre_collection': { + category: 'checkpoint', + description: 'The adversary is trying to collect data of interest to achieve his goal. ', + name: 'checkpoint.mitre_collection', + type: 'keyword', + }, + 'checkpoint.mitre_command_and_control': { + category: 'checkpoint', + description: + 'The adversary is trying to communicate with compromised systems in order to control them. ', + name: 'checkpoint.mitre_command_and_control', + type: 'keyword', + }, + 'checkpoint.mitre_exfiltration': { + category: 'checkpoint', + description: 'The adversary is trying to steal data. ', + name: 'checkpoint.mitre_exfiltration', + type: 'keyword', + }, + 'checkpoint.mitre_impact': { + category: 'checkpoint', + description: + 'The adversary is trying to manipulate, interrupt, or destroy your systems and data. ', + name: 'checkpoint.mitre_impact', + type: 'keyword', + }, + 'checkpoint.parent_file_hash': { + category: 'checkpoint', + description: "Archive's hash in case of extracted files. ", + name: 'checkpoint.parent_file_hash', + type: 'keyword', + }, + 'checkpoint.parent_file_name': { + category: 'checkpoint', + description: "Archive's name in case of extracted files. ", + name: 'checkpoint.parent_file_name', + type: 'keyword', + }, + 'checkpoint.parent_file_uid': { + category: 'checkpoint', + description: "Archive's UID in case of extracted files. ", + name: 'checkpoint.parent_file_uid', + type: 'keyword', + }, + 'checkpoint.similiar_iocs': { + category: 'checkpoint', + description: 'Other IoCs similar to the ones found, related to the malicious file. ', + name: 'checkpoint.similiar_iocs', + type: 'keyword', + }, + 'checkpoint.similar_hashes': { + category: 'checkpoint', + description: 'Hashes found similar to the malicious file. ', + name: 'checkpoint.similar_hashes', + type: 'keyword', + }, + 'checkpoint.similar_strings': { + category: 'checkpoint', + description: 'Strings found similar to the malicious file. ', + name: 'checkpoint.similar_strings', + type: 'keyword', + }, + 'checkpoint.similar_communication': { + category: 'checkpoint', + description: 'Network action found similar to the malicious file. ', + name: 'checkpoint.similar_communication', + type: 'keyword', + }, + 'checkpoint.te_verdict_determined_by': { + category: 'checkpoint', + description: 'Emulators determined file verdict. ', + name: 'checkpoint.te_verdict_determined_by', + type: 'keyword', + }, + 'checkpoint.packet_capture_unique_id': { + category: 'checkpoint', + description: 'Identifier of the packet capture files. ', + name: 'checkpoint.packet_capture_unique_id', + type: 'keyword', + }, + 'checkpoint.total_attachments': { + category: 'checkpoint', + description: 'The number of attachments in an email. ', + name: 'checkpoint.total_attachments', + type: 'integer', + }, + 'checkpoint.additional_info': { + category: 'checkpoint', + description: 'ID of original file/mail which are sent by admin. ', + name: 'checkpoint.additional_info', + type: 'keyword', + }, + 'checkpoint.content_risk': { + category: 'checkpoint', + description: 'File risk. ', + name: 'checkpoint.content_risk', + type: 'integer', + }, + 'checkpoint.operation': { + category: 'checkpoint', + description: 'Operation made by Threat Extraction. ', + name: 'checkpoint.operation', + type: 'keyword', + }, + 'checkpoint.scrubbed_content': { + category: 'checkpoint', + description: 'Active content that was found. ', + name: 'checkpoint.scrubbed_content', + type: 'keyword', + }, + 'checkpoint.scrub_time': { + category: 'checkpoint', + description: 'Extraction process duration. ', + name: 'checkpoint.scrub_time', + type: 'keyword', + }, + 'checkpoint.scrub_download_time': { + category: 'checkpoint', + description: 'File download time from resource. ', + name: 'checkpoint.scrub_download_time', + type: 'keyword', + }, + 'checkpoint.scrub_total_time': { + category: 'checkpoint', + description: 'Threat extraction total file handling time. ', + name: 'checkpoint.scrub_total_time', + type: 'keyword', + }, + 'checkpoint.scrub_activity': { + category: 'checkpoint', + description: 'The result of the extraction ', + name: 'checkpoint.scrub_activity', + type: 'keyword', + }, + 'checkpoint.watermark': { + category: 'checkpoint', + description: 'Reports whether watermark is added to the cleaned file. ', + name: 'checkpoint.watermark', + type: 'keyword', + }, + 'checkpoint.source_object': { + category: 'checkpoint', + description: 'Matched object name on source column. ', + name: 'checkpoint.source_object', + type: 'integer', + }, + 'checkpoint.destination_object': { + category: 'checkpoint', + description: 'Matched object name on destination column. ', + name: 'checkpoint.destination_object', + type: 'keyword', + }, + 'checkpoint.drop_reason': { + category: 'checkpoint', + description: 'Drop reason description. ', + name: 'checkpoint.drop_reason', + type: 'keyword', + }, + 'checkpoint.hit': { + category: 'checkpoint', + description: 'Number of hits on a rule. ', + name: 'checkpoint.hit', + type: 'integer', + }, + 'checkpoint.rulebase_id': { + category: 'checkpoint', + description: 'Layer number. ', + name: 'checkpoint.rulebase_id', + type: 'integer', + }, + 'checkpoint.first_hit_time': { + category: 'checkpoint', + description: 'First hit time in current interval. ', + name: 'checkpoint.first_hit_time', + type: 'integer', + }, + 'checkpoint.last_hit_time': { + category: 'checkpoint', + description: 'Last hit time in current interval. ', + name: 'checkpoint.last_hit_time', + type: 'integer', + }, + 'checkpoint.rematch_info': { + category: 'checkpoint', + description: + 'Information sent when old connections cannot be matched during policy installation. ', + name: 'checkpoint.rematch_info', + type: 'keyword', + }, + 'checkpoint.last_rematch_time': { + category: 'checkpoint', + description: 'Connection rematched time. ', + name: 'checkpoint.last_rematch_time', + type: 'keyword', + }, + 'checkpoint.action_reason': { + category: 'checkpoint', + description: 'Connection drop reason. ', + name: 'checkpoint.action_reason', + type: 'integer', + }, + 'checkpoint.c_bytes': { + category: 'checkpoint', + description: 'Boolean value indicates whether bytes sent from the client side are used. ', + name: 'checkpoint.c_bytes', + type: 'integer', + }, + 'checkpoint.context_num': { + category: 'checkpoint', + description: 'Serial number of the log for a specific connection. ', + name: 'checkpoint.context_num', + type: 'integer', + }, + 'checkpoint.match_id': { + category: 'checkpoint', + description: 'Private key of the rule ', + name: 'checkpoint.match_id', + type: 'integer', + }, + 'checkpoint.alert': { + category: 'checkpoint', + description: 'Alert level of matched rule (for connection logs). ', + name: 'checkpoint.alert', + type: 'keyword', + }, + 'checkpoint.parent_rule': { + category: 'checkpoint', + description: 'Parent rule number, in case of inline layer. ', + name: 'checkpoint.parent_rule', + type: 'integer', + }, + 'checkpoint.match_fk': { + category: 'checkpoint', + description: 'Rule number. ', + name: 'checkpoint.match_fk', + type: 'integer', + }, + 'checkpoint.dropped_outgoing': { + category: 'checkpoint', + description: 'Number of outgoing bytes dropped when using UP-limit feature. ', + name: 'checkpoint.dropped_outgoing', + type: 'integer', + }, + 'checkpoint.dropped_incoming': { + category: 'checkpoint', + description: 'Number of incoming bytes dropped when using UP-limit feature. ', + name: 'checkpoint.dropped_incoming', + type: 'integer', + }, + 'checkpoint.media_type': { + category: 'checkpoint', + description: 'Media used (audio, video, etc.) ', + name: 'checkpoint.media_type', + type: 'keyword', + }, + 'checkpoint.sip_reason': { + category: 'checkpoint', + description: "Explains why 'source_ip' isn't allowed to redirect (handover). ", + name: 'checkpoint.sip_reason', + type: 'keyword', + }, + 'checkpoint.voip_method': { + category: 'checkpoint', + description: 'Registration request. ', + name: 'checkpoint.voip_method', + type: 'keyword', + }, + 'checkpoint.registered_ip-phones': { + category: 'checkpoint', + description: 'Registered IP-Phones. ', + name: 'checkpoint.registered_ip-phones', + type: 'keyword', + }, + 'checkpoint.voip_reg_user_type': { + category: 'checkpoint', + description: 'Registered IP-Phone type. ', + name: 'checkpoint.voip_reg_user_type', + type: 'keyword', + }, + 'checkpoint.voip_call_id': { + category: 'checkpoint', + description: 'Call-ID. ', + name: 'checkpoint.voip_call_id', + type: 'keyword', + }, + 'checkpoint.voip_reg_int': { + category: 'checkpoint', + description: 'Registration port. ', + name: 'checkpoint.voip_reg_int', + type: 'integer', + }, + 'checkpoint.voip_reg_ipp': { + category: 'checkpoint', + description: 'Registration IP protocol. ', + name: 'checkpoint.voip_reg_ipp', + type: 'integer', + }, + 'checkpoint.voip_reg_period': { + category: 'checkpoint', + description: 'Registration period. ', + name: 'checkpoint.voip_reg_period', + type: 'integer', + }, + 'checkpoint.src_phone_number': { + category: 'checkpoint', + description: 'Source IP-Phone. ', + name: 'checkpoint.src_phone_number', + type: 'keyword', + }, + 'checkpoint.voip_from_user_type': { + category: 'checkpoint', + description: 'Source IP-Phone type. ', + name: 'checkpoint.voip_from_user_type', + type: 'keyword', + }, + 'checkpoint.voip_to_user_type': { + category: 'checkpoint', + description: 'Destination IP-Phone type. ', + name: 'checkpoint.voip_to_user_type', + type: 'keyword', + }, + 'checkpoint.voip_call_dir': { + category: 'checkpoint', + description: 'Call direction: in/out. ', + name: 'checkpoint.voip_call_dir', + type: 'keyword', + }, + 'checkpoint.voip_call_state': { + category: 'checkpoint', + description: 'Call state. Possible values: in/out. ', + name: 'checkpoint.voip_call_state', + type: 'keyword', + }, + 'checkpoint.voip_call_term_time': { + category: 'checkpoint', + description: 'Call termination time stamp. ', + name: 'checkpoint.voip_call_term_time', + type: 'keyword', + }, + 'checkpoint.voip_duration': { + category: 'checkpoint', + description: 'Call duration (seconds). ', + name: 'checkpoint.voip_duration', + type: 'keyword', + }, + 'checkpoint.voip_media_port': { + category: 'checkpoint', + description: 'Media int. ', + name: 'checkpoint.voip_media_port', + type: 'keyword', + }, + 'checkpoint.voip_media_ipp': { + category: 'checkpoint', + description: 'Media IP protocol. ', + name: 'checkpoint.voip_media_ipp', + type: 'keyword', + }, + 'checkpoint.voip_est_codec': { + category: 'checkpoint', + description: 'Estimated codec. ', + name: 'checkpoint.voip_est_codec', + type: 'keyword', + }, + 'checkpoint.voip_exp': { + category: 'checkpoint', + description: 'Expiration. ', + name: 'checkpoint.voip_exp', + type: 'integer', + }, + 'checkpoint.voip_attach_sz': { + category: 'checkpoint', + description: 'Attachment size. ', + name: 'checkpoint.voip_attach_sz', + type: 'integer', + }, + 'checkpoint.voip_attach_action_info': { + category: 'checkpoint', + description: 'Attachment action Info. ', + name: 'checkpoint.voip_attach_action_info', + type: 'keyword', + }, + 'checkpoint.voip_media_codec': { + category: 'checkpoint', + description: 'Estimated codec. ', + name: 'checkpoint.voip_media_codec', + type: 'keyword', + }, + 'checkpoint.voip_reject_reason': { + category: 'checkpoint', + description: 'Reject reason. ', + name: 'checkpoint.voip_reject_reason', + type: 'keyword', + }, + 'checkpoint.voip_reason_info': { + category: 'checkpoint', + description: 'Information. ', + name: 'checkpoint.voip_reason_info', + type: 'keyword', + }, + 'checkpoint.voip_config': { + category: 'checkpoint', + description: 'Configuration. ', + name: 'checkpoint.voip_config', + type: 'keyword', + }, + 'checkpoint.voip_reg_server': { + category: 'checkpoint', + description: 'Registrar server IP address. ', + name: 'checkpoint.voip_reg_server', + type: 'ip', + }, + 'checkpoint.scv_user': { + category: 'checkpoint', + description: 'Username whose packets are dropped on SCV. ', + name: 'checkpoint.scv_user', + type: 'keyword', + }, + 'checkpoint.scv_message_info': { + category: 'checkpoint', + description: 'Drop reason. ', + name: 'checkpoint.scv_message_info', + type: 'keyword', + }, + 'checkpoint.ppp': { + category: 'checkpoint', + description: 'Authentication status. ', + name: 'checkpoint.ppp', + type: 'keyword', + }, + 'checkpoint.scheme': { + category: 'checkpoint', + description: 'Describes the scheme used for the log. ', + name: 'checkpoint.scheme', + type: 'keyword', + }, + 'checkpoint.machine': { + category: 'checkpoint', + description: 'L2TP machine which triggered the log and the log refers to it. ', + name: 'checkpoint.machine', + type: 'keyword', + }, + 'checkpoint.vpn_feature_name': { + category: 'checkpoint', + description: 'L2TP /IKE / Link Selection. ', + name: 'checkpoint.vpn_feature_name', + type: 'keyword', + }, + 'checkpoint.reject_category': { + category: 'checkpoint', + description: 'Authentication failure reason. ', + name: 'checkpoint.reject_category', + type: 'keyword', + }, + 'checkpoint.peer_ip_probing_status_update': { + category: 'checkpoint', + description: 'IP address response status. ', + name: 'checkpoint.peer_ip_probing_status_update', + type: 'keyword', + }, + 'checkpoint.peer_ip': { + category: 'checkpoint', + description: 'IP address which the client connects to. ', + name: 'checkpoint.peer_ip', + type: 'keyword', + }, + 'checkpoint.link_probing_status_update': { + category: 'checkpoint', + description: 'IP address response status. ', + name: 'checkpoint.link_probing_status_update', + type: 'keyword', + }, + 'checkpoint.source_interface': { + category: 'checkpoint', + description: 'External Interface name for source interface or Null if not found. ', + name: 'checkpoint.source_interface', + type: 'keyword', + }, + 'checkpoint.next_hop_ip': { + category: 'checkpoint', + description: 'Next hop IP address. ', + name: 'checkpoint.next_hop_ip', + type: 'keyword', + }, + 'checkpoint.srckeyid': { + category: 'checkpoint', + description: 'Initiator Spi ID. ', + name: 'checkpoint.srckeyid', + type: 'keyword', + }, + 'checkpoint.dstkeyid': { + category: 'checkpoint', + description: 'Responder Spi ID. ', + name: 'checkpoint.dstkeyid', + type: 'keyword', + }, + 'checkpoint.encryption_failure': { + category: 'checkpoint', + description: 'Message indicating why the encryption failed. ', + name: 'checkpoint.encryption_failure', + type: 'keyword', + }, + 'checkpoint.ike_ids': { + category: 'checkpoint', + description: 'All QM ids. ', + name: 'checkpoint.ike_ids', + type: 'keyword', + }, + 'checkpoint.community': { + category: 'checkpoint', + description: 'Community name for the IPSec key and the use of the IKEv. ', + name: 'checkpoint.community', + type: 'keyword', + }, + 'checkpoint.ike': { + category: 'checkpoint', + description: 'IKEMode (PHASE1, PHASE2, etc..). ', + name: 'checkpoint.ike', + type: 'keyword', + }, + 'checkpoint.cookieI': { + category: 'checkpoint', + description: 'Initiator cookie. ', + name: 'checkpoint.cookieI', + type: 'keyword', + }, + 'checkpoint.cookieR': { + category: 'checkpoint', + description: 'Responder cookie. ', + name: 'checkpoint.cookieR', + type: 'keyword', + }, + 'checkpoint.msgid': { + category: 'checkpoint', + description: 'Message ID. ', + name: 'checkpoint.msgid', + type: 'keyword', + }, + 'checkpoint.methods': { + category: 'checkpoint', + description: 'IPSEc methods. ', + name: 'checkpoint.methods', + type: 'keyword', + }, + 'checkpoint.connection_uid': { + category: 'checkpoint', + description: 'Calculation of md5 of the IP and user name as UID. ', + name: 'checkpoint.connection_uid', + type: 'keyword', + }, + 'checkpoint.site_name': { + category: 'checkpoint', + description: 'Site name. ', + name: 'checkpoint.site_name', + type: 'keyword', + }, + 'checkpoint.esod_rule_name': { + category: 'checkpoint', + description: 'Unknown rule name. ', + name: 'checkpoint.esod_rule_name', + type: 'keyword', + }, + 'checkpoint.esod_rule_action': { + category: 'checkpoint', + description: 'Unknown rule action. ', + name: 'checkpoint.esod_rule_action', + type: 'keyword', + }, + 'checkpoint.esod_rule_type': { + category: 'checkpoint', + description: 'Unknown rule type. ', + name: 'checkpoint.esod_rule_type', + type: 'keyword', + }, + 'checkpoint.esod_noncompliance_reason': { + category: 'checkpoint', + description: 'Non-compliance reason. ', + name: 'checkpoint.esod_noncompliance_reason', + type: 'keyword', + }, + 'checkpoint.esod_associated_policies': { + category: 'checkpoint', + description: 'Associated policies. ', + name: 'checkpoint.esod_associated_policies', + type: 'keyword', + }, + 'checkpoint.spyware_type': { + category: 'checkpoint', + description: 'Spyware type. ', + name: 'checkpoint.spyware_type', + type: 'keyword', + }, + 'checkpoint.anti_virus_type': { + category: 'checkpoint', + description: 'Anti virus type. ', + name: 'checkpoint.anti_virus_type', + type: 'keyword', + }, + 'checkpoint.end_user_firewall_type': { + category: 'checkpoint', + description: 'End user firewall type. ', + name: 'checkpoint.end_user_firewall_type', + type: 'keyword', + }, + 'checkpoint.esod_scan_status': { + category: 'checkpoint', + description: 'Scan failed. ', + name: 'checkpoint.esod_scan_status', + type: 'keyword', + }, + 'checkpoint.esod_access_status': { + category: 'checkpoint', + description: 'Access denied. ', + name: 'checkpoint.esod_access_status', + type: 'keyword', + }, + 'checkpoint.client_type': { + category: 'checkpoint', + description: 'Endpoint Connect. ', + name: 'checkpoint.client_type', + type: 'keyword', + }, + 'checkpoint.precise_error': { + category: 'checkpoint', + description: 'HTTP parser error. ', + name: 'checkpoint.precise_error', + type: 'keyword', + }, + 'checkpoint.method': { + category: 'checkpoint', + description: 'HTTP method. ', + name: 'checkpoint.method', + type: 'keyword', + }, + 'checkpoint.trusted_domain': { + category: 'checkpoint', + description: 'In case of phishing event, the domain, which the attacker was impersonating. ', + name: 'checkpoint.trusted_domain', + type: 'keyword', + }, + 'cisco.asa.message_id': { + category: 'cisco', + description: 'The Cisco ASA message identifier. ', + name: 'cisco.asa.message_id', + type: 'keyword', + }, + 'cisco.asa.suffix': { + category: 'cisco', + description: 'Optional suffix after %ASA identifier. ', + example: 'session', + name: 'cisco.asa.suffix', + type: 'keyword', + }, + 'cisco.asa.source_interface': { + category: 'cisco', + description: 'Source interface for the flow or event. ', + name: 'cisco.asa.source_interface', + type: 'keyword', + }, + 'cisco.asa.destination_interface': { + category: 'cisco', + description: 'Destination interface for the flow or event. ', + name: 'cisco.asa.destination_interface', + type: 'keyword', + }, + 'cisco.asa.rule_name': { + category: 'cisco', + description: 'Name of the Access Control List rule that matched this event. ', + name: 'cisco.asa.rule_name', + type: 'keyword', + }, + 'cisco.asa.source_username': { + category: 'cisco', + description: 'Name of the user that is the source for this event. ', + name: 'cisco.asa.source_username', + type: 'keyword', + }, + 'cisco.asa.destination_username': { + category: 'cisco', + description: 'Name of the user that is the destination for this event. ', + name: 'cisco.asa.destination_username', + type: 'keyword', + }, + 'cisco.asa.mapped_source_ip': { + category: 'cisco', + description: 'The translated source IP address. ', + name: 'cisco.asa.mapped_source_ip', + type: 'ip', + }, + 'cisco.asa.mapped_source_host': { + category: 'cisco', + description: 'The translated source host. ', + name: 'cisco.asa.mapped_source_host', + type: 'keyword', + }, + 'cisco.asa.mapped_source_port': { + category: 'cisco', + description: 'The translated source port. ', + name: 'cisco.asa.mapped_source_port', + type: 'long', + }, + 'cisco.asa.mapped_destination_ip': { + category: 'cisco', + description: 'The translated destination IP address. ', + name: 'cisco.asa.mapped_destination_ip', + type: 'ip', + }, + 'cisco.asa.mapped_destination_host': { + category: 'cisco', + description: 'The translated destination host. ', + name: 'cisco.asa.mapped_destination_host', + type: 'keyword', + }, + 'cisco.asa.mapped_destination_port': { + category: 'cisco', + description: 'The translated destination port. ', + name: 'cisco.asa.mapped_destination_port', + type: 'long', + }, + 'cisco.asa.threat_level': { + category: 'cisco', + description: + 'Threat level for malware / botnet traffic. One of very-low, low, moderate, high or very-high. ', + name: 'cisco.asa.threat_level', + type: 'keyword', + }, + 'cisco.asa.threat_category': { + category: 'cisco', + description: + 'Category for the malware / botnet traffic. For example: virus, botnet, trojan, etc. ', + name: 'cisco.asa.threat_category', + type: 'keyword', + }, + 'cisco.asa.connection_id': { + category: 'cisco', + description: 'Unique identifier for a flow. ', + name: 'cisco.asa.connection_id', + type: 'keyword', + }, + 'cisco.asa.icmp_type': { + category: 'cisco', + description: 'ICMP type. ', + name: 'cisco.asa.icmp_type', + type: 'short', + }, + 'cisco.asa.icmp_code': { + category: 'cisco', + description: 'ICMP code. ', + name: 'cisco.asa.icmp_code', + type: 'short', + }, + 'cisco.asa.connection_type': { + category: 'cisco', + description: 'The VPN connection type ', + name: 'cisco.asa.connection_type', + type: 'keyword', + }, + 'cisco.asa.dap_records': { + category: 'cisco', + description: 'The assigned DAP records ', + name: 'cisco.asa.dap_records', + type: 'keyword', + }, + 'cisco.ftd.message_id': { + category: 'cisco', + description: 'The Cisco FTD message identifier. ', + name: 'cisco.ftd.message_id', + type: 'keyword', + }, + 'cisco.ftd.suffix': { + category: 'cisco', + description: 'Optional suffix after %FTD identifier. ', + example: 'session', + name: 'cisco.ftd.suffix', + type: 'keyword', + }, + 'cisco.ftd.source_interface': { + category: 'cisco', + description: 'Source interface for the flow or event. ', + name: 'cisco.ftd.source_interface', + type: 'keyword', + }, + 'cisco.ftd.destination_interface': { + category: 'cisco', + description: 'Destination interface for the flow or event. ', + name: 'cisco.ftd.destination_interface', + type: 'keyword', + }, + 'cisco.ftd.rule_name': { + category: 'cisco', + description: 'Name of the Access Control List rule that matched this event. ', + name: 'cisco.ftd.rule_name', + type: 'keyword', + }, + 'cisco.ftd.source_username': { + category: 'cisco', + description: 'Name of the user that is the source for this event. ', + name: 'cisco.ftd.source_username', + type: 'keyword', + }, + 'cisco.ftd.destination_username': { + category: 'cisco', + description: 'Name of the user that is the destination for this event. ', + name: 'cisco.ftd.destination_username', + type: 'keyword', + }, + 'cisco.ftd.mapped_source_ip': { + category: 'cisco', + description: 'The translated source IP address. Use ECS source.nat.ip. ', + name: 'cisco.ftd.mapped_source_ip', + type: 'ip', + }, + 'cisco.ftd.mapped_source_host': { + category: 'cisco', + description: 'The translated source host. ', + name: 'cisco.ftd.mapped_source_host', + type: 'keyword', + }, + 'cisco.ftd.mapped_source_port': { + category: 'cisco', + description: 'The translated source port. Use ECS source.nat.port. ', + name: 'cisco.ftd.mapped_source_port', + type: 'long', + }, + 'cisco.ftd.mapped_destination_ip': { + category: 'cisco', + description: 'The translated destination IP address. Use ECS destination.nat.ip. ', + name: 'cisco.ftd.mapped_destination_ip', + type: 'ip', + }, + 'cisco.ftd.mapped_destination_host': { + category: 'cisco', + description: 'The translated destination host. ', + name: 'cisco.ftd.mapped_destination_host', + type: 'keyword', + }, + 'cisco.ftd.mapped_destination_port': { + category: 'cisco', + description: 'The translated destination port. Use ECS destination.nat.port. ', + name: 'cisco.ftd.mapped_destination_port', + type: 'long', + }, + 'cisco.ftd.threat_level': { + category: 'cisco', + description: + 'Threat level for malware / botnet traffic. One of very-low, low, moderate, high or very-high. ', + name: 'cisco.ftd.threat_level', + type: 'keyword', + }, + 'cisco.ftd.threat_category': { + category: 'cisco', + description: + 'Category for the malware / botnet traffic. For example: virus, botnet, trojan, etc. ', + name: 'cisco.ftd.threat_category', + type: 'keyword', + }, + 'cisco.ftd.connection_id': { + category: 'cisco', + description: 'Unique identifier for a flow. ', + name: 'cisco.ftd.connection_id', + type: 'keyword', + }, + 'cisco.ftd.icmp_type': { + category: 'cisco', + description: 'ICMP type. ', + name: 'cisco.ftd.icmp_type', + type: 'short', + }, + 'cisco.ftd.icmp_code': { + category: 'cisco', + description: 'ICMP code. ', + name: 'cisco.ftd.icmp_code', + type: 'short', + }, + 'cisco.ftd.security': { + category: 'cisco', + description: 'Raw fields for Security Events.', + name: 'cisco.ftd.security', + type: 'object', + }, + 'cisco.ftd.connection_type': { + category: 'cisco', + description: 'The VPN connection type ', + name: 'cisco.ftd.connection_type', + type: 'keyword', + }, + 'cisco.ftd.dap_records': { + category: 'cisco', + description: 'The assigned DAP records ', + name: 'cisco.ftd.dap_records', + type: 'keyword', + }, + 'cisco.ios.access_list': { + category: 'cisco', + description: 'Name of the IP access list. ', + name: 'cisco.ios.access_list', + type: 'keyword', + }, + 'cisco.ios.facility': { + category: 'cisco', + description: + 'The facility to which the message refers (for example, SNMP, SYS, and so forth). A facility can be a hardware device, a protocol, or a module of the system software. It denotes the source or the cause of the system message. ', + example: 'SEC', + name: 'cisco.ios.facility', + type: 'keyword', + }, + 'coredns.id': { + category: 'coredns', + description: 'id of the DNS transaction ', + name: 'coredns.id', + type: 'keyword', + }, + 'coredns.query.size': { + category: 'coredns', + description: 'size of the DNS query ', + name: 'coredns.query.size', + type: 'integer', + format: 'bytes', + }, + 'coredns.query.class': { + category: 'coredns', + description: 'DNS query class ', + name: 'coredns.query.class', + type: 'keyword', + }, + 'coredns.query.name': { + category: 'coredns', + description: 'DNS query name ', + name: 'coredns.query.name', + type: 'keyword', + }, + 'coredns.query.type': { + category: 'coredns', + description: 'DNS query type ', + name: 'coredns.query.type', + type: 'keyword', + }, + 'coredns.response.code': { + category: 'coredns', + description: 'DNS response code ', + name: 'coredns.response.code', + type: 'keyword', + }, + 'coredns.response.flags': { + category: 'coredns', + description: 'DNS response flags ', + name: 'coredns.response.flags', + type: 'keyword', + }, + 'coredns.response.size': { + category: 'coredns', + description: 'size of the DNS response ', + name: 'coredns.response.size', + type: 'integer', + format: 'bytes', + }, + 'coredns.dnssec_ok': { + category: 'coredns', + description: 'dnssec flag ', + name: 'coredns.dnssec_ok', + type: 'boolean', + }, + 'crowdstrike.metadata.eventType': { + category: 'crowdstrike', + description: + 'DetectionSummaryEvent, FirewallMatchEvent, IncidentSummaryEvent, RemoteResponseSessionStartEvent, RemoteResponseSessionEndEvent, AuthActivityAuditEvent, or UserActivityAuditEvent ', + name: 'crowdstrike.metadata.eventType', + type: 'keyword', + }, + 'crowdstrike.metadata.eventCreationTime': { + category: 'crowdstrike', + description: 'The time this event occurred on the endpoint in UTC UNIX_MS format. ', + name: 'crowdstrike.metadata.eventCreationTime', + type: 'date', + }, + 'crowdstrike.metadata.offset': { + category: 'crowdstrike', + description: + 'Offset number that tracks the location of the event in stream. This is used to identify unique detection events. ', + name: 'crowdstrike.metadata.offset', + type: 'integer', + }, + 'crowdstrike.metadata.customerIDString': { + category: 'crowdstrike', + description: 'Customer identifier ', + name: 'crowdstrike.metadata.customerIDString', + type: 'keyword', + }, + 'crowdstrike.metadata.version': { + category: 'crowdstrike', + description: 'Schema version ', + name: 'crowdstrike.metadata.version', + type: 'keyword', + }, + 'crowdstrike.event.ProcessStartTime': { + category: 'crowdstrike', + description: 'The process start time in UTC UNIX_MS format. ', + name: 'crowdstrike.event.ProcessStartTime', + type: 'date', + }, + 'crowdstrike.event.ProcessEndTime': { + category: 'crowdstrike', + description: 'The process termination time in UTC UNIX_MS format. ', + name: 'crowdstrike.event.ProcessEndTime', + type: 'date', + }, + 'crowdstrike.event.ProcessId': { + category: 'crowdstrike', + description: 'Process ID related to the detection. ', + name: 'crowdstrike.event.ProcessId', + type: 'integer', + }, + 'crowdstrike.event.ParentProcessId': { + category: 'crowdstrike', + description: 'Parent process ID related to the detection. ', + name: 'crowdstrike.event.ParentProcessId', + type: 'integer', + }, + 'crowdstrike.event.ComputerName': { + category: 'crowdstrike', + description: 'Name of the computer where the detection occurred. ', + name: 'crowdstrike.event.ComputerName', + type: 'keyword', + }, + 'crowdstrike.event.UserName': { + category: 'crowdstrike', + description: 'User name associated with the detection. ', + name: 'crowdstrike.event.UserName', + type: 'keyword', + }, + 'crowdstrike.event.DetectName': { + category: 'crowdstrike', + description: 'Name of the detection. ', + name: 'crowdstrike.event.DetectName', + type: 'keyword', + }, + 'crowdstrike.event.DetectDescription': { + category: 'crowdstrike', + description: 'Description of the detection. ', + name: 'crowdstrike.event.DetectDescription', + type: 'keyword', + }, + 'crowdstrike.event.Severity': { + category: 'crowdstrike', + description: 'Severity score of the detection. ', + name: 'crowdstrike.event.Severity', + type: 'integer', + }, + 'crowdstrike.event.SeverityName': { + category: 'crowdstrike', + description: 'Severity score text. ', + name: 'crowdstrike.event.SeverityName', + type: 'keyword', + }, + 'crowdstrike.event.FileName': { + category: 'crowdstrike', + description: 'File name of the associated process for the detection. ', + name: 'crowdstrike.event.FileName', + type: 'keyword', + }, + 'crowdstrike.event.FilePath': { + category: 'crowdstrike', + description: 'Path of the executable associated with the detection. ', + name: 'crowdstrike.event.FilePath', + type: 'keyword', + }, + 'crowdstrike.event.CommandLine': { + category: 'crowdstrike', + description: 'Executable path with command line arguments. ', + name: 'crowdstrike.event.CommandLine', + type: 'keyword', + }, + 'crowdstrike.event.SHA1String': { + category: 'crowdstrike', + description: 'SHA1 sum of the executable associated with the detection. ', + name: 'crowdstrike.event.SHA1String', + type: 'keyword', + }, + 'crowdstrike.event.SHA256String': { + category: 'crowdstrike', + description: 'SHA256 sum of the executable associated with the detection. ', + name: 'crowdstrike.event.SHA256String', + type: 'keyword', + }, + 'crowdstrike.event.MD5String': { + category: 'crowdstrike', + description: 'MD5 sum of the executable associated with the detection. ', + name: 'crowdstrike.event.MD5String', + type: 'keyword', + }, + 'crowdstrike.event.MachineDomain': { + category: 'crowdstrike', + description: 'Domain for the machine associated with the detection. ', + name: 'crowdstrike.event.MachineDomain', + type: 'keyword', + }, + 'crowdstrike.event.FalconHostLink': { + category: 'crowdstrike', + description: 'URL to view the detection in Falcon. ', + name: 'crowdstrike.event.FalconHostLink', + type: 'keyword', + }, + 'crowdstrike.event.SensorId': { + category: 'crowdstrike', + description: 'Unique ID associated with the Falcon sensor. ', + name: 'crowdstrike.event.SensorId', + type: 'keyword', + }, + 'crowdstrike.event.DetectId': { + category: 'crowdstrike', + description: 'Unique ID associated with the detection. ', + name: 'crowdstrike.event.DetectId', + type: 'keyword', + }, + 'crowdstrike.event.LocalIP': { + category: 'crowdstrike', + description: 'IP address of the host associated with the detection. ', + name: 'crowdstrike.event.LocalIP', + type: 'keyword', + }, + 'crowdstrike.event.MACAddress': { + category: 'crowdstrike', + description: 'MAC address of the host associated with the detection. ', + name: 'crowdstrike.event.MACAddress', + type: 'keyword', + }, + 'crowdstrike.event.Tactic': { + category: 'crowdstrike', + description: 'MITRE tactic category of the detection. ', + name: 'crowdstrike.event.Tactic', + type: 'keyword', + }, + 'crowdstrike.event.Technique': { + category: 'crowdstrike', + description: 'MITRE technique category of the detection. ', + name: 'crowdstrike.event.Technique', + type: 'keyword', + }, + 'crowdstrike.event.Objective': { + category: 'crowdstrike', + description: 'Method of detection. ', + name: 'crowdstrike.event.Objective', + type: 'keyword', + }, + 'crowdstrike.event.PatternDispositionDescription': { + category: 'crowdstrike', + description: 'Action taken by Falcon. ', + name: 'crowdstrike.event.PatternDispositionDescription', + type: 'keyword', + }, + 'crowdstrike.event.PatternDispositionValue': { + category: 'crowdstrike', + description: 'Unique ID associated with action taken. ', + name: 'crowdstrike.event.PatternDispositionValue', + type: 'integer', + }, + 'crowdstrike.event.PatternDispositionFlags': { + category: 'crowdstrike', + description: 'Flags indicating actions taken. ', + name: 'crowdstrike.event.PatternDispositionFlags', + type: 'object', + }, + 'crowdstrike.event.State': { + category: 'crowdstrike', + description: 'Whether the incident summary is open and ongoing or closed. ', + name: 'crowdstrike.event.State', + type: 'keyword', + }, + 'crowdstrike.event.IncidentStartTime': { + category: 'crowdstrike', + description: 'Start time for the incident in UTC UNIX format. ', + name: 'crowdstrike.event.IncidentStartTime', + type: 'date', + }, + 'crowdstrike.event.IncidentEndTime': { + category: 'crowdstrike', + description: 'End time for the incident in UTC UNIX format. ', + name: 'crowdstrike.event.IncidentEndTime', + type: 'date', + }, + 'crowdstrike.event.FineScore': { + category: 'crowdstrike', + description: 'Score for incident. ', + name: 'crowdstrike.event.FineScore', + type: 'float', + }, + 'crowdstrike.event.UserId': { + category: 'crowdstrike', + description: 'Email address or user ID associated with the event. ', + name: 'crowdstrike.event.UserId', + type: 'keyword', + }, + 'crowdstrike.event.UserIp': { + category: 'crowdstrike', + description: 'IP address associated with the user. ', + name: 'crowdstrike.event.UserIp', + type: 'keyword', + }, + 'crowdstrike.event.OperationName': { + category: 'crowdstrike', + description: 'Event subtype. ', + name: 'crowdstrike.event.OperationName', + type: 'keyword', + }, + 'crowdstrike.event.ServiceName': { + category: 'crowdstrike', + description: 'Service associated with this event. ', + name: 'crowdstrike.event.ServiceName', + type: 'keyword', + }, + 'crowdstrike.event.Success': { + category: 'crowdstrike', + description: 'Indicator of whether or not this event was successful. ', + name: 'crowdstrike.event.Success', + type: 'boolean', + }, + 'crowdstrike.event.UTCTimestamp': { + category: 'crowdstrike', + description: 'Timestamp associated with this event in UTC UNIX format. ', + name: 'crowdstrike.event.UTCTimestamp', + type: 'date', + }, + 'crowdstrike.event.AuditKeyValues': { + category: 'crowdstrike', + description: 'Fields that were changed in this event. ', + name: 'crowdstrike.event.AuditKeyValues', + type: 'nested', + }, + 'crowdstrike.event.ExecutablesWritten': { + category: 'crowdstrike', + description: 'Detected executables written to disk by a process. ', + name: 'crowdstrike.event.ExecutablesWritten', + type: 'nested', + }, + 'crowdstrike.event.SessionId': { + category: 'crowdstrike', + description: 'Session ID of the remote response session. ', + name: 'crowdstrike.event.SessionId', + type: 'keyword', + }, + 'crowdstrike.event.HostnameField': { + category: 'crowdstrike', + description: 'Host name of the machine for the remote session. ', + name: 'crowdstrike.event.HostnameField', + type: 'keyword', + }, + 'crowdstrike.event.StartTimestamp': { + category: 'crowdstrike', + description: 'Start time for the remote session in UTC UNIX format. ', + name: 'crowdstrike.event.StartTimestamp', + type: 'date', + }, + 'crowdstrike.event.EndTimestamp': { + category: 'crowdstrike', + description: 'End time for the remote session in UTC UNIX format. ', + name: 'crowdstrike.event.EndTimestamp', + type: 'date', + }, + 'crowdstrike.event.LateralMovement': { + category: 'crowdstrike', + description: 'Lateral movement field for incident. ', + name: 'crowdstrike.event.LateralMovement', + type: 'long', + }, + 'crowdstrike.event.ParentImageFileName': { + category: 'crowdstrike', + description: 'Path to the parent process. ', + name: 'crowdstrike.event.ParentImageFileName', + type: 'keyword', + }, + 'crowdstrike.event.ParentCommandLine': { + category: 'crowdstrike', + description: 'Parent process command line arguments. ', + name: 'crowdstrike.event.ParentCommandLine', + type: 'keyword', + }, + 'crowdstrike.event.GrandparentImageFileName': { + category: 'crowdstrike', + description: 'Path to the grandparent process. ', + name: 'crowdstrike.event.GrandparentImageFileName', + type: 'keyword', + }, + 'crowdstrike.event.GrandparentCommandLine': { + category: 'crowdstrike', + description: 'Grandparent process command line arguments. ', + name: 'crowdstrike.event.GrandparentCommandLine', + type: 'keyword', + }, + 'crowdstrike.event.IOCType': { + category: 'crowdstrike', + description: 'CrowdStrike type for indicator of compromise. ', + name: 'crowdstrike.event.IOCType', + type: 'keyword', + }, + 'crowdstrike.event.IOCValue': { + category: 'crowdstrike', + description: 'CrowdStrike value for indicator of compromise. ', + name: 'crowdstrike.event.IOCValue', + type: 'keyword', + }, + 'crowdstrike.event.CustomerId': { + category: 'crowdstrike', + description: 'Customer identifier. ', + name: 'crowdstrike.event.CustomerId', + type: 'keyword', + }, + 'crowdstrike.event.DeviceId': { + category: 'crowdstrike', + description: 'Device on which the event occurred. ', + name: 'crowdstrike.event.DeviceId', + type: 'keyword', + }, + 'crowdstrike.event.Ipv': { + category: 'crowdstrike', + description: 'Protocol for network request. ', + name: 'crowdstrike.event.Ipv', + type: 'keyword', + }, + 'crowdstrike.event.ConnectionDirection': { + category: 'crowdstrike', + description: 'Direction for network connection. ', + name: 'crowdstrike.event.ConnectionDirection', + type: 'keyword', + }, + 'crowdstrike.event.EventType': { + category: 'crowdstrike', + description: 'CrowdStrike provided event type. ', + name: 'crowdstrike.event.EventType', + type: 'keyword', + }, + 'crowdstrike.event.HostName': { + category: 'crowdstrike', + description: 'Host name of the local machine. ', + name: 'crowdstrike.event.HostName', + type: 'keyword', + }, + 'crowdstrike.event.ICMPCode': { + category: 'crowdstrike', + description: 'RFC2780 ICMP Code field. ', + name: 'crowdstrike.event.ICMPCode', + type: 'keyword', + }, + 'crowdstrike.event.ICMPType': { + category: 'crowdstrike', + description: 'RFC2780 ICMP Type field. ', + name: 'crowdstrike.event.ICMPType', + type: 'keyword', + }, + 'crowdstrike.event.ImageFileName': { + category: 'crowdstrike', + description: 'File name of the associated process for the detection. ', + name: 'crowdstrike.event.ImageFileName', + type: 'keyword', + }, + 'crowdstrike.event.PID': { + category: 'crowdstrike', + description: 'Associated process id for the detection. ', + name: 'crowdstrike.event.PID', + type: 'long', + }, + 'crowdstrike.event.LocalAddress': { + category: 'crowdstrike', + description: 'IP address of local machine. ', + name: 'crowdstrike.event.LocalAddress', + type: 'ip', + }, + 'crowdstrike.event.LocalPort': { + category: 'crowdstrike', + description: 'Port of local machine. ', + name: 'crowdstrike.event.LocalPort', + type: 'long', + }, + 'crowdstrike.event.RemoteAddress': { + category: 'crowdstrike', + description: 'IP address of remote machine. ', + name: 'crowdstrike.event.RemoteAddress', + type: 'ip', + }, + 'crowdstrike.event.RemotePort': { + category: 'crowdstrike', + description: 'Port of remote machine. ', + name: 'crowdstrike.event.RemotePort', + type: 'long', + }, + 'crowdstrike.event.RuleAction': { + category: 'crowdstrike', + description: 'Firewall rule action. ', + name: 'crowdstrike.event.RuleAction', + type: 'keyword', + }, + 'crowdstrike.event.RuleDescription': { + category: 'crowdstrike', + description: 'Firewall rule description. ', + name: 'crowdstrike.event.RuleDescription', + type: 'keyword', + }, + 'crowdstrike.event.RuleFamilyID': { + category: 'crowdstrike', + description: 'Firewall rule family id. ', + name: 'crowdstrike.event.RuleFamilyID', + type: 'keyword', + }, + 'crowdstrike.event.RuleGroupName': { + category: 'crowdstrike', + description: 'Firewall rule group name. ', + name: 'crowdstrike.event.RuleGroupName', + type: 'keyword', + }, + 'crowdstrike.event.RuleName': { + category: 'crowdstrike', + description: 'Firewall rule name. ', + name: 'crowdstrike.event.RuleName', + type: 'keyword', + }, + 'crowdstrike.event.RuleId': { + category: 'crowdstrike', + description: 'Firewall rule id. ', + name: 'crowdstrike.event.RuleId', + type: 'keyword', + }, + 'crowdstrike.event.MatchCount': { + category: 'crowdstrike', + description: 'Number of firewall rule matches. ', + name: 'crowdstrike.event.MatchCount', + type: 'long', + }, + 'crowdstrike.event.MatchCountSinceLastReport': { + category: 'crowdstrike', + description: 'Number of firewall rule matches since the last report. ', + name: 'crowdstrike.event.MatchCountSinceLastReport', + type: 'long', + }, + 'crowdstrike.event.Timestamp': { + category: 'crowdstrike', + description: 'Firewall rule triggered timestamp. ', + name: 'crowdstrike.event.Timestamp', + type: 'date', + }, + 'crowdstrike.event.Flags.Audit': { + category: 'crowdstrike', + description: 'CrowdStrike audit flag. ', + name: 'crowdstrike.event.Flags.Audit', + type: 'boolean', + }, + 'crowdstrike.event.Flags.Log': { + category: 'crowdstrike', + description: 'CrowdStrike log flag. ', + name: 'crowdstrike.event.Flags.Log', + type: 'boolean', + }, + 'crowdstrike.event.Flags.Monitor': { + category: 'crowdstrike', + description: 'CrowdStrike monitor flag. ', + name: 'crowdstrike.event.Flags.Monitor', + type: 'boolean', + }, + 'crowdstrike.event.Protocol': { + category: 'crowdstrike', + description: 'CrowdStrike provided protocol. ', + name: 'crowdstrike.event.Protocol', + type: 'keyword', + }, + 'crowdstrike.event.NetworkProfile': { + category: 'crowdstrike', + description: 'CrowdStrike network profile. ', + name: 'crowdstrike.event.NetworkProfile', + type: 'keyword', + }, + 'crowdstrike.event.PolicyName': { + category: 'crowdstrike', + description: 'CrowdStrike policy name. ', + name: 'crowdstrike.event.PolicyName', + type: 'keyword', + }, + 'crowdstrike.event.PolicyID': { + category: 'crowdstrike', + description: 'CrowdStrike policy id. ', + name: 'crowdstrike.event.PolicyID', + type: 'keyword', + }, + 'crowdstrike.event.Status': { + category: 'crowdstrike', + description: 'CrowdStrike status. ', + name: 'crowdstrike.event.Status', + type: 'keyword', + }, + 'crowdstrike.event.TreeID': { + category: 'crowdstrike', + description: 'CrowdStrike tree id. ', + name: 'crowdstrike.event.TreeID', + type: 'keyword', + }, + 'crowdstrike.event.Commands': { + category: 'crowdstrike', + description: 'Commands run in a remote session. ', + name: 'crowdstrike.event.Commands', + type: 'keyword', + }, + 'envoyproxy.log_type': { + category: 'envoyproxy', + description: 'Envoy log type, normally ACCESS ', + name: 'envoyproxy.log_type', + type: 'keyword', + }, + 'envoyproxy.response_flags': { + category: 'envoyproxy', + description: 'Response flags ', + name: 'envoyproxy.response_flags', + type: 'keyword', + }, + 'envoyproxy.upstream_service_time': { + category: 'envoyproxy', + description: 'Upstream service time in nanoseconds ', + name: 'envoyproxy.upstream_service_time', + type: 'long', + format: 'duration', + }, + 'envoyproxy.request_id': { + category: 'envoyproxy', + description: 'ID of the request ', + name: 'envoyproxy.request_id', + type: 'keyword', + }, + 'envoyproxy.authority': { + category: 'envoyproxy', + description: 'Envoy proxy authority field ', + name: 'envoyproxy.authority', + type: 'keyword', + }, + 'envoyproxy.proxy_type': { + category: 'envoyproxy', + description: 'Envoy proxy type, tcp or http ', + name: 'envoyproxy.proxy_type', + type: 'keyword', + }, + 'fortinet.file.hash.crc32': { + category: 'fortinet', + description: 'CRC32 Hash of file ', + name: 'fortinet.file.hash.crc32', + type: 'keyword', + }, + 'fortinet.firewall.acct_stat': { + category: 'fortinet', + description: 'Accounting state (RADIUS) ', + name: 'fortinet.firewall.acct_stat', + type: 'keyword', + }, + 'fortinet.firewall.acktime': { + category: 'fortinet', + description: 'Alarm Acknowledge Time ', + name: 'fortinet.firewall.acktime', + type: 'keyword', + }, + 'fortinet.firewall.act': { + category: 'fortinet', + description: 'Action ', + name: 'fortinet.firewall.act', + type: 'keyword', + }, + 'fortinet.firewall.action': { + category: 'fortinet', + description: 'Status of the session ', + name: 'fortinet.firewall.action', + type: 'keyword', + }, + 'fortinet.firewall.activity': { + category: 'fortinet', + description: 'HA activity message ', + name: 'fortinet.firewall.activity', + type: 'keyword', + }, + 'fortinet.firewall.addr': { + category: 'fortinet', + description: 'IP Address ', + name: 'fortinet.firewall.addr', + type: 'ip', + }, + 'fortinet.firewall.addr_type': { + category: 'fortinet', + description: 'Address Type ', + name: 'fortinet.firewall.addr_type', + type: 'keyword', + }, + 'fortinet.firewall.addrgrp': { + category: 'fortinet', + description: 'Address Group ', + name: 'fortinet.firewall.addrgrp', + type: 'keyword', + }, + 'fortinet.firewall.adgroup': { + category: 'fortinet', + description: 'AD Group Name ', + name: 'fortinet.firewall.adgroup', + type: 'keyword', + }, + 'fortinet.firewall.admin': { + category: 'fortinet', + description: 'Admin User ', + name: 'fortinet.firewall.admin', + type: 'keyword', + }, + 'fortinet.firewall.age': { + category: 'fortinet', + description: 'Time in seconds - time passed since last seen ', + name: 'fortinet.firewall.age', + type: 'integer', + }, + 'fortinet.firewall.agent': { + category: 'fortinet', + description: 'User agent - eg. agent="Mozilla/5.0" ', + name: 'fortinet.firewall.agent', + type: 'keyword', + }, + 'fortinet.firewall.alarmid': { + category: 'fortinet', + description: 'Alarm ID ', + name: 'fortinet.firewall.alarmid', + type: 'integer', + }, + 'fortinet.firewall.alert': { + category: 'fortinet', + description: 'Alert ', + name: 'fortinet.firewall.alert', + type: 'keyword', + }, + 'fortinet.firewall.analyticscksum': { + category: 'fortinet', + description: 'The checksum of the file submitted for analytics ', + name: 'fortinet.firewall.analyticscksum', + type: 'keyword', + }, + 'fortinet.firewall.analyticssubmit': { + category: 'fortinet', + description: 'The flag for analytics submission ', + name: 'fortinet.firewall.analyticssubmit', + type: 'keyword', + }, + 'fortinet.firewall.ap': { + category: 'fortinet', + description: 'Access Point ', + name: 'fortinet.firewall.ap', + type: 'keyword', + }, + 'fortinet.firewall.app-type': { + category: 'fortinet', + description: 'Address Type ', + name: 'fortinet.firewall.app-type', + type: 'keyword', + }, + 'fortinet.firewall.appact': { + category: 'fortinet', + description: 'The security action from app control ', + name: 'fortinet.firewall.appact', + type: 'keyword', + }, + 'fortinet.firewall.appid': { + category: 'fortinet', + description: 'Application ID ', + name: 'fortinet.firewall.appid', + type: 'integer', + }, + 'fortinet.firewall.applist': { + category: 'fortinet', + description: 'Application Control profile ', + name: 'fortinet.firewall.applist', + type: 'keyword', + }, + 'fortinet.firewall.apprisk': { + category: 'fortinet', + description: 'Application Risk Level ', + name: 'fortinet.firewall.apprisk', + type: 'keyword', + }, + 'fortinet.firewall.apscan': { + category: 'fortinet', + description: 'The name of the AP, which scanned and detected the rogue AP ', + name: 'fortinet.firewall.apscan', + type: 'keyword', + }, + 'fortinet.firewall.apsn': { + category: 'fortinet', + description: 'Access Point ', + name: 'fortinet.firewall.apsn', + type: 'keyword', + }, + 'fortinet.firewall.apstatus': { + category: 'fortinet', + description: 'Access Point status ', + name: 'fortinet.firewall.apstatus', + type: 'keyword', + }, + 'fortinet.firewall.aptype': { + category: 'fortinet', + description: 'Access Point type ', + name: 'fortinet.firewall.aptype', + type: 'keyword', + }, + 'fortinet.firewall.assigned': { + category: 'fortinet', + description: 'Assigned IP Address ', + name: 'fortinet.firewall.assigned', + type: 'ip', + }, + 'fortinet.firewall.assignip': { + category: 'fortinet', + description: 'Assigned IP Address ', + name: 'fortinet.firewall.assignip', + type: 'ip', + }, + 'fortinet.firewall.attachment': { + category: 'fortinet', + description: 'The flag for email attachement ', + name: 'fortinet.firewall.attachment', + type: 'keyword', + }, + 'fortinet.firewall.attack': { + category: 'fortinet', + description: 'Attack Name ', + name: 'fortinet.firewall.attack', + type: 'keyword', + }, + 'fortinet.firewall.attackcontext': { + category: 'fortinet', + description: 'The trigger patterns and the packetdata with base64 encoding ', + name: 'fortinet.firewall.attackcontext', + type: 'keyword', + }, + 'fortinet.firewall.attackcontextid': { + category: 'fortinet', + description: 'Attack context id / total ', + name: 'fortinet.firewall.attackcontextid', + type: 'keyword', + }, + 'fortinet.firewall.attackid': { + category: 'fortinet', + description: 'Attack ID ', + name: 'fortinet.firewall.attackid', + type: 'integer', + }, + 'fortinet.firewall.auditid': { + category: 'fortinet', + description: 'Audit ID ', + name: 'fortinet.firewall.auditid', + type: 'long', + }, + 'fortinet.firewall.auditscore': { + category: 'fortinet', + description: 'The Audit Score ', + name: 'fortinet.firewall.auditscore', + type: 'keyword', + }, + 'fortinet.firewall.audittime': { + category: 'fortinet', + description: 'The time of the audit ', + name: 'fortinet.firewall.audittime', + type: 'long', + }, + 'fortinet.firewall.authgrp': { + category: 'fortinet', + description: 'Authorization Group ', + name: 'fortinet.firewall.authgrp', + type: 'keyword', + }, + 'fortinet.firewall.authid': { + category: 'fortinet', + description: 'Authentication ID ', + name: 'fortinet.firewall.authid', + type: 'keyword', + }, + 'fortinet.firewall.authproto': { + category: 'fortinet', + description: 'The protocol that initiated the authentication ', + name: 'fortinet.firewall.authproto', + type: 'keyword', + }, + 'fortinet.firewall.authserver': { + category: 'fortinet', + description: 'Authentication server ', + name: 'fortinet.firewall.authserver', + type: 'keyword', + }, + 'fortinet.firewall.bandwidth': { + category: 'fortinet', + description: 'Bandwidth ', + name: 'fortinet.firewall.bandwidth', + type: 'keyword', + }, + 'fortinet.firewall.banned_rule': { + category: 'fortinet', + description: 'NAC quarantine Banned Rule Name ', + name: 'fortinet.firewall.banned_rule', + type: 'keyword', + }, + 'fortinet.firewall.banned_src': { + category: 'fortinet', + description: 'NAC quarantine Banned Source IP ', + name: 'fortinet.firewall.banned_src', + type: 'keyword', + }, + 'fortinet.firewall.banword': { + category: 'fortinet', + description: 'Banned word ', + name: 'fortinet.firewall.banword', + type: 'keyword', + }, + 'fortinet.firewall.botnetdomain': { + category: 'fortinet', + description: 'Botnet Domain Name ', + name: 'fortinet.firewall.botnetdomain', + type: 'keyword', + }, + 'fortinet.firewall.botnetip': { + category: 'fortinet', + description: 'Botnet IP Address ', + name: 'fortinet.firewall.botnetip', + type: 'ip', + }, + 'fortinet.firewall.bssid': { + category: 'fortinet', + description: 'Service Set ID ', + name: 'fortinet.firewall.bssid', + type: 'keyword', + }, + 'fortinet.firewall.call_id': { + category: 'fortinet', + description: 'Caller ID ', + name: 'fortinet.firewall.call_id', + type: 'keyword', + }, + 'fortinet.firewall.carrier_ep': { + category: 'fortinet', + description: 'The FortiOS Carrier end-point identification ', + name: 'fortinet.firewall.carrier_ep', + type: 'keyword', + }, + 'fortinet.firewall.cat': { + category: 'fortinet', + description: 'DNS category ID ', + name: 'fortinet.firewall.cat', + type: 'integer', + }, + 'fortinet.firewall.category': { + category: 'fortinet', + description: 'Authentication category ', + name: 'fortinet.firewall.category', + type: 'keyword', + }, + 'fortinet.firewall.cc': { + category: 'fortinet', + description: 'CC Email Address ', + name: 'fortinet.firewall.cc', + type: 'keyword', + }, + 'fortinet.firewall.cdrcontent': { + category: 'fortinet', + description: 'Cdrcontent ', + name: 'fortinet.firewall.cdrcontent', + type: 'keyword', + }, + 'fortinet.firewall.centralnatid': { + category: 'fortinet', + description: 'Central NAT ID ', + name: 'fortinet.firewall.centralnatid', + type: 'integer', + }, + 'fortinet.firewall.cert': { + category: 'fortinet', + description: 'Certificate ', + name: 'fortinet.firewall.cert', + type: 'keyword', + }, + 'fortinet.firewall.cert-type': { + category: 'fortinet', + description: 'Certificate type ', + name: 'fortinet.firewall.cert-type', + type: 'keyword', + }, + 'fortinet.firewall.certhash': { + category: 'fortinet', + description: 'Certificate hash ', + name: 'fortinet.firewall.certhash', + type: 'keyword', + }, + 'fortinet.firewall.cfgattr': { + category: 'fortinet', + description: 'Configuration attribute ', + name: 'fortinet.firewall.cfgattr', + type: 'keyword', + }, + 'fortinet.firewall.cfgobj': { + category: 'fortinet', + description: 'Configuration object ', + name: 'fortinet.firewall.cfgobj', + type: 'keyword', + }, + 'fortinet.firewall.cfgpath': { + category: 'fortinet', + description: 'Configuration path ', + name: 'fortinet.firewall.cfgpath', + type: 'keyword', + }, + 'fortinet.firewall.cfgtid': { + category: 'fortinet', + description: 'Configuration transaction ID ', + name: 'fortinet.firewall.cfgtid', + type: 'keyword', + }, + 'fortinet.firewall.cfgtxpower': { + category: 'fortinet', + description: 'Configuration TX power ', + name: 'fortinet.firewall.cfgtxpower', + type: 'integer', + }, + 'fortinet.firewall.channel': { + category: 'fortinet', + description: 'Wireless Channel ', + name: 'fortinet.firewall.channel', + type: 'integer', + }, + 'fortinet.firewall.channeltype': { + category: 'fortinet', + description: 'SSH channel type ', + name: 'fortinet.firewall.channeltype', + type: 'keyword', + }, + 'fortinet.firewall.chassisid': { + category: 'fortinet', + description: 'Chassis ID ', + name: 'fortinet.firewall.chassisid', + type: 'integer', + }, + 'fortinet.firewall.checksum': { + category: 'fortinet', + description: 'The checksum of the scanned file ', + name: 'fortinet.firewall.checksum', + type: 'keyword', + }, + 'fortinet.firewall.chgheaders': { + category: 'fortinet', + description: 'HTTP Headers ', + name: 'fortinet.firewall.chgheaders', + type: 'keyword', + }, + 'fortinet.firewall.cldobjid': { + category: 'fortinet', + description: 'Connector object ID ', + name: 'fortinet.firewall.cldobjid', + type: 'keyword', + }, + 'fortinet.firewall.client_addr': { + category: 'fortinet', + description: 'Wifi client address ', + name: 'fortinet.firewall.client_addr', + type: 'keyword', + }, + 'fortinet.firewall.cloudaction': { + category: 'fortinet', + description: 'Cloud Action ', + name: 'fortinet.firewall.cloudaction', + type: 'keyword', + }, + 'fortinet.firewall.clouduser': { + category: 'fortinet', + description: 'Cloud User ', + name: 'fortinet.firewall.clouduser', + type: 'keyword', + }, + 'fortinet.firewall.column': { + category: 'fortinet', + description: 'VOIP Column ', + name: 'fortinet.firewall.column', + type: 'integer', + }, + 'fortinet.firewall.command': { + category: 'fortinet', + description: 'CLI Command ', + name: 'fortinet.firewall.command', + type: 'keyword', + }, + 'fortinet.firewall.community': { + category: 'fortinet', + description: 'SNMP Community ', + name: 'fortinet.firewall.community', + type: 'keyword', + }, + 'fortinet.firewall.configcountry': { + category: 'fortinet', + description: 'Configuration country ', + name: 'fortinet.firewall.configcountry', + type: 'keyword', + }, + 'fortinet.firewall.connection_type': { + category: 'fortinet', + description: 'FortiClient Connection Type ', + name: 'fortinet.firewall.connection_type', + type: 'keyword', + }, + 'fortinet.firewall.conserve': { + category: 'fortinet', + description: 'Flag for conserve mode ', + name: 'fortinet.firewall.conserve', + type: 'keyword', + }, + 'fortinet.firewall.constraint': { + category: 'fortinet', + description: 'WAF http protocol restrictions ', + name: 'fortinet.firewall.constraint', + type: 'keyword', + }, + 'fortinet.firewall.contentdisarmed': { + category: 'fortinet', + description: 'Email scanned content ', + name: 'fortinet.firewall.contentdisarmed', + type: 'keyword', + }, + 'fortinet.firewall.contenttype': { + category: 'fortinet', + description: 'Content Type from HTTP header ', + name: 'fortinet.firewall.contenttype', + type: 'keyword', + }, + 'fortinet.firewall.cookies': { + category: 'fortinet', + description: 'VPN Cookie ', + name: 'fortinet.firewall.cookies', + type: 'keyword', + }, + 'fortinet.firewall.count': { + category: 'fortinet', + description: 'Counts of action type ', + name: 'fortinet.firewall.count', + type: 'integer', + }, + 'fortinet.firewall.countapp': { + category: 'fortinet', + description: 'Number of App Ctrl logs associated with the session ', + name: 'fortinet.firewall.countapp', + type: 'integer', + }, + 'fortinet.firewall.countav': { + category: 'fortinet', + description: 'Number of AV logs associated with the session ', + name: 'fortinet.firewall.countav', + type: 'integer', + }, + 'fortinet.firewall.countcifs': { + category: 'fortinet', + description: 'Number of CIFS logs associated with the session ', + name: 'fortinet.firewall.countcifs', + type: 'integer', + }, + 'fortinet.firewall.countdlp': { + category: 'fortinet', + description: 'Number of DLP logs associated with the session ', + name: 'fortinet.firewall.countdlp', + type: 'integer', + }, + 'fortinet.firewall.countdns': { + category: 'fortinet', + description: 'Number of DNS logs associated with the session ', + name: 'fortinet.firewall.countdns', + type: 'integer', + }, + 'fortinet.firewall.countemail': { + category: 'fortinet', + description: 'Number of email logs associated with the session ', + name: 'fortinet.firewall.countemail', + type: 'integer', + }, + 'fortinet.firewall.countff': { + category: 'fortinet', + description: 'Number of ff logs associated with the session ', + name: 'fortinet.firewall.countff', + type: 'integer', + }, + 'fortinet.firewall.countips': { + category: 'fortinet', + description: 'Number of IPS logs associated with the session ', + name: 'fortinet.firewall.countips', + type: 'integer', + }, + 'fortinet.firewall.countssh': { + category: 'fortinet', + description: 'Number of SSH logs associated with the session ', + name: 'fortinet.firewall.countssh', + type: 'integer', + }, + 'fortinet.firewall.countssl': { + category: 'fortinet', + description: 'Number of SSL logs associated with the session ', + name: 'fortinet.firewall.countssl', + type: 'integer', + }, + 'fortinet.firewall.countwaf': { + category: 'fortinet', + description: 'Number of WAF logs associated with the session ', + name: 'fortinet.firewall.countwaf', + type: 'integer', + }, + 'fortinet.firewall.countweb': { + category: 'fortinet', + description: 'Number of Web filter logs associated with the session ', + name: 'fortinet.firewall.countweb', + type: 'integer', + }, + 'fortinet.firewall.cpu': { + category: 'fortinet', + description: 'CPU Usage ', + name: 'fortinet.firewall.cpu', + type: 'integer', + }, + 'fortinet.firewall.craction': { + category: 'fortinet', + description: 'Client Reputation Action ', + name: 'fortinet.firewall.craction', + type: 'integer', + }, + 'fortinet.firewall.criticalcount': { + category: 'fortinet', + description: 'Number of critical ratings ', + name: 'fortinet.firewall.criticalcount', + type: 'integer', + }, + 'fortinet.firewall.crl': { + category: 'fortinet', + description: 'Client Reputation Level ', + name: 'fortinet.firewall.crl', + type: 'keyword', + }, + 'fortinet.firewall.crlevel': { + category: 'fortinet', + description: 'Client Reputation Level ', + name: 'fortinet.firewall.crlevel', + type: 'keyword', + }, + 'fortinet.firewall.crscore': { + category: 'fortinet', + description: 'Some description ', + name: 'fortinet.firewall.crscore', + type: 'integer', + }, + 'fortinet.firewall.cveid': { + category: 'fortinet', + description: 'CVE ID ', + name: 'fortinet.firewall.cveid', + type: 'keyword', + }, + 'fortinet.firewall.daemon': { + category: 'fortinet', + description: 'Daemon name ', + name: 'fortinet.firewall.daemon', + type: 'keyword', + }, + 'fortinet.firewall.datarange': { + category: 'fortinet', + description: 'Data range for reports ', + name: 'fortinet.firewall.datarange', + type: 'keyword', + }, + 'fortinet.firewall.date': { + category: 'fortinet', + description: 'Date ', + name: 'fortinet.firewall.date', + type: 'keyword', + }, + 'fortinet.firewall.ddnsserver': { + category: 'fortinet', + description: 'DDNS server ', + name: 'fortinet.firewall.ddnsserver', + type: 'ip', + }, + 'fortinet.firewall.desc': { + category: 'fortinet', + description: 'Description ', + name: 'fortinet.firewall.desc', + type: 'keyword', + }, + 'fortinet.firewall.detectionmethod': { + category: 'fortinet', + description: 'Detection method ', + name: 'fortinet.firewall.detectionmethod', + type: 'keyword', + }, + 'fortinet.firewall.devcategory': { + category: 'fortinet', + description: 'Device category ', + name: 'fortinet.firewall.devcategory', + type: 'keyword', + }, + 'fortinet.firewall.devintfname': { + category: 'fortinet', + description: 'HA device Interface Name ', + name: 'fortinet.firewall.devintfname', + type: 'keyword', + }, + 'fortinet.firewall.devtype': { + category: 'fortinet', + description: 'Device type ', + name: 'fortinet.firewall.devtype', + type: 'keyword', + }, + 'fortinet.firewall.dhcp_msg': { + category: 'fortinet', + description: 'DHCP Message ', + name: 'fortinet.firewall.dhcp_msg', + type: 'keyword', + }, + 'fortinet.firewall.dintf': { + category: 'fortinet', + description: 'Destination interface ', + name: 'fortinet.firewall.dintf', + type: 'keyword', + }, + 'fortinet.firewall.disk': { + category: 'fortinet', + description: 'Assosciated disk ', + name: 'fortinet.firewall.disk', + type: 'keyword', + }, + 'fortinet.firewall.disklograte': { + category: 'fortinet', + description: 'Disk logging rate ', + name: 'fortinet.firewall.disklograte', + type: 'long', + }, + 'fortinet.firewall.dlpextra': { + category: 'fortinet', + description: 'DLP extra information ', + name: 'fortinet.firewall.dlpextra', + type: 'keyword', + }, + 'fortinet.firewall.docsource': { + category: 'fortinet', + description: 'DLP fingerprint document source ', + name: 'fortinet.firewall.docsource', + type: 'keyword', + }, + 'fortinet.firewall.domainctrlauthstate': { + category: 'fortinet', + description: 'CIFS domain auth state ', + name: 'fortinet.firewall.domainctrlauthstate', + type: 'integer', + }, + 'fortinet.firewall.domainctrlauthtype': { + category: 'fortinet', + description: 'CIFS domain auth type ', + name: 'fortinet.firewall.domainctrlauthtype', + type: 'integer', + }, + 'fortinet.firewall.domainctrldomain': { + category: 'fortinet', + description: 'CIFS domain auth domain ', + name: 'fortinet.firewall.domainctrldomain', + type: 'keyword', + }, + 'fortinet.firewall.domainctrlip': { + category: 'fortinet', + description: 'CIFS Domain IP ', + name: 'fortinet.firewall.domainctrlip', + type: 'ip', + }, + 'fortinet.firewall.domainctrlname': { + category: 'fortinet', + description: 'CIFS Domain name ', + name: 'fortinet.firewall.domainctrlname', + type: 'keyword', + }, + 'fortinet.firewall.domainctrlprotocoltype': { + category: 'fortinet', + description: 'CIFS Domain connection protocol ', + name: 'fortinet.firewall.domainctrlprotocoltype', + type: 'integer', + }, + 'fortinet.firewall.domainctrlusername': { + category: 'fortinet', + description: 'CIFS Domain username ', + name: 'fortinet.firewall.domainctrlusername', + type: 'keyword', + }, + 'fortinet.firewall.domainfilteridx': { + category: 'fortinet', + description: 'Domain filter ID ', + name: 'fortinet.firewall.domainfilteridx', + type: 'integer', + }, + 'fortinet.firewall.domainfilterlist': { + category: 'fortinet', + description: 'Domain filter name ', + name: 'fortinet.firewall.domainfilterlist', + type: 'keyword', + }, + 'fortinet.firewall.ds': { + category: 'fortinet', + description: 'Direction with distribution system ', + name: 'fortinet.firewall.ds', + type: 'keyword', + }, + 'fortinet.firewall.dst_int': { + category: 'fortinet', + description: 'Destination interface ', + name: 'fortinet.firewall.dst_int', + type: 'keyword', + }, + 'fortinet.firewall.dstintfrole': { + category: 'fortinet', + description: 'Destination interface role ', + name: 'fortinet.firewall.dstintfrole', + type: 'keyword', + }, + 'fortinet.firewall.dstcountry': { + category: 'fortinet', + description: 'Destination country ', + name: 'fortinet.firewall.dstcountry', + type: 'keyword', + }, + 'fortinet.firewall.dstdevcategory': { + category: 'fortinet', + description: 'Destination device category ', + name: 'fortinet.firewall.dstdevcategory', + type: 'keyword', + }, + 'fortinet.firewall.dstdevtype': { + category: 'fortinet', + description: 'Destination device type ', + name: 'fortinet.firewall.dstdevtype', + type: 'keyword', + }, + 'fortinet.firewall.dstfamily': { + category: 'fortinet', + description: 'Destination OS family ', + name: 'fortinet.firewall.dstfamily', + type: 'keyword', + }, + 'fortinet.firewall.dsthwvendor': { + category: 'fortinet', + description: 'Destination HW vendor ', + name: 'fortinet.firewall.dsthwvendor', + type: 'keyword', + }, + 'fortinet.firewall.dsthwversion': { + category: 'fortinet', + description: 'Destination HW version ', + name: 'fortinet.firewall.dsthwversion', + type: 'keyword', + }, + 'fortinet.firewall.dstinetsvc': { + category: 'fortinet', + description: 'Destination interface service ', + name: 'fortinet.firewall.dstinetsvc', + type: 'keyword', + }, + 'fortinet.firewall.dstosname': { + category: 'fortinet', + description: 'Destination OS name ', + name: 'fortinet.firewall.dstosname', + type: 'keyword', + }, + 'fortinet.firewall.dstosversion': { + category: 'fortinet', + description: 'Destination OS version ', + name: 'fortinet.firewall.dstosversion', + type: 'keyword', + }, + 'fortinet.firewall.dstserver': { + category: 'fortinet', + description: 'Destination server ', + name: 'fortinet.firewall.dstserver', + type: 'integer', + }, + 'fortinet.firewall.dstssid': { + category: 'fortinet', + description: 'Destination SSID ', + name: 'fortinet.firewall.dstssid', + type: 'keyword', + }, + 'fortinet.firewall.dstswversion': { + category: 'fortinet', + description: 'Destination software version ', + name: 'fortinet.firewall.dstswversion', + type: 'keyword', + }, + 'fortinet.firewall.dstunauthusersource': { + category: 'fortinet', + description: 'Destination unauthenticated source ', + name: 'fortinet.firewall.dstunauthusersource', + type: 'keyword', + }, + 'fortinet.firewall.dstuuid': { + category: 'fortinet', + description: 'UUID of the Destination IP address ', + name: 'fortinet.firewall.dstuuid', + type: 'keyword', + }, + 'fortinet.firewall.duid': { + category: 'fortinet', + description: 'DHCP UID ', + name: 'fortinet.firewall.duid', + type: 'keyword', + }, + 'fortinet.firewall.eapolcnt': { + category: 'fortinet', + description: 'EAPOL packet count ', + name: 'fortinet.firewall.eapolcnt', + type: 'integer', + }, + 'fortinet.firewall.eapoltype': { + category: 'fortinet', + description: 'EAPOL packet type ', + name: 'fortinet.firewall.eapoltype', + type: 'keyword', + }, + 'fortinet.firewall.encrypt': { + category: 'fortinet', + description: 'Whether the packet is encrypted or not ', + name: 'fortinet.firewall.encrypt', + type: 'integer', + }, + 'fortinet.firewall.encryption': { + category: 'fortinet', + description: 'Encryption method ', + name: 'fortinet.firewall.encryption', + type: 'keyword', + }, + 'fortinet.firewall.epoch': { + category: 'fortinet', + description: 'Epoch used for locating file ', + name: 'fortinet.firewall.epoch', + type: 'integer', + }, + 'fortinet.firewall.espauth': { + category: 'fortinet', + description: 'ESP Authentication ', + name: 'fortinet.firewall.espauth', + type: 'keyword', + }, + 'fortinet.firewall.esptransform': { + category: 'fortinet', + description: 'ESP Transform ', + name: 'fortinet.firewall.esptransform', + type: 'keyword', + }, + 'fortinet.firewall.exch': { + category: 'fortinet', + description: 'Mail Exchanges from DNS response answer section ', + name: 'fortinet.firewall.exch', + type: 'keyword', + }, + 'fortinet.firewall.exchange': { + category: 'fortinet', + description: 'Mail Exchanges from DNS response answer section ', + name: 'fortinet.firewall.exchange', + type: 'keyword', + }, + 'fortinet.firewall.expectedsignature': { + category: 'fortinet', + description: 'Expected SSL signature ', + name: 'fortinet.firewall.expectedsignature', + type: 'keyword', + }, + 'fortinet.firewall.expiry': { + category: 'fortinet', + description: 'FortiGuard override expiry timestamp ', + name: 'fortinet.firewall.expiry', + type: 'keyword', + }, + 'fortinet.firewall.fams_pause': { + category: 'fortinet', + description: 'Fortinet Analysis and Management Service Pause ', + name: 'fortinet.firewall.fams_pause', + type: 'integer', + }, + 'fortinet.firewall.fazlograte': { + category: 'fortinet', + description: 'FortiAnalyzer Logging Rate ', + name: 'fortinet.firewall.fazlograte', + type: 'long', + }, + 'fortinet.firewall.fctemssn': { + category: 'fortinet', + description: 'FortiClient Endpoint SSN ', + name: 'fortinet.firewall.fctemssn', + type: 'keyword', + }, + 'fortinet.firewall.fctuid': { + category: 'fortinet', + description: 'FortiClient UID ', + name: 'fortinet.firewall.fctuid', + type: 'keyword', + }, + 'fortinet.firewall.field': { + category: 'fortinet', + description: 'NTP status field ', + name: 'fortinet.firewall.field', + type: 'keyword', + }, + 'fortinet.firewall.filefilter': { + category: 'fortinet', + description: 'The filter used to identify the affected file ', + name: 'fortinet.firewall.filefilter', + type: 'keyword', + }, + 'fortinet.firewall.filehashsrc': { + category: 'fortinet', + description: 'Filehash source ', + name: 'fortinet.firewall.filehashsrc', + type: 'keyword', + }, + 'fortinet.firewall.filtercat': { + category: 'fortinet', + description: 'DLP filter category ', + name: 'fortinet.firewall.filtercat', + type: 'keyword', + }, + 'fortinet.firewall.filteridx': { + category: 'fortinet', + description: 'DLP filter ID ', + name: 'fortinet.firewall.filteridx', + type: 'integer', + }, + 'fortinet.firewall.filtername': { + category: 'fortinet', + description: 'DLP rule name ', + name: 'fortinet.firewall.filtername', + type: 'keyword', + }, + 'fortinet.firewall.filtertype': { + category: 'fortinet', + description: 'DLP filter type ', + name: 'fortinet.firewall.filtertype', + type: 'keyword', + }, + 'fortinet.firewall.fortiguardresp': { + category: 'fortinet', + description: 'Antispam ESP value ', + name: 'fortinet.firewall.fortiguardresp', + type: 'keyword', + }, + 'fortinet.firewall.forwardedfor': { + category: 'fortinet', + description: 'Email address forwarded ', + name: 'fortinet.firewall.forwardedfor', + type: 'keyword', + }, + 'fortinet.firewall.fqdn': { + category: 'fortinet', + description: 'FQDN ', + name: 'fortinet.firewall.fqdn', + type: 'keyword', + }, + 'fortinet.firewall.frametype': { + category: 'fortinet', + description: 'Wireless frametype ', + name: 'fortinet.firewall.frametype', + type: 'keyword', + }, + 'fortinet.firewall.freediskstorage': { + category: 'fortinet', + description: 'Free disk integer ', + name: 'fortinet.firewall.freediskstorage', + type: 'integer', + }, + 'fortinet.firewall.from': { + category: 'fortinet', + description: 'From email address ', + name: 'fortinet.firewall.from', + type: 'keyword', + }, + 'fortinet.firewall.from_vcluster': { + category: 'fortinet', + description: 'Source virtual cluster number ', + name: 'fortinet.firewall.from_vcluster', + type: 'integer', + }, + 'fortinet.firewall.fsaverdict': { + category: 'fortinet', + description: 'FSA verdict ', + name: 'fortinet.firewall.fsaverdict', + type: 'keyword', + }, + 'fortinet.firewall.fwserver_name': { + category: 'fortinet', + description: 'Web proxy server name ', + name: 'fortinet.firewall.fwserver_name', + type: 'keyword', + }, + 'fortinet.firewall.gateway': { + category: 'fortinet', + description: 'Gateway ip address for PPPoE status report ', + name: 'fortinet.firewall.gateway', + type: 'ip', + }, + 'fortinet.firewall.green': { + category: 'fortinet', + description: 'Memory status ', + name: 'fortinet.firewall.green', + type: 'keyword', + }, + 'fortinet.firewall.groupid': { + category: 'fortinet', + description: 'User Group ID ', + name: 'fortinet.firewall.groupid', + type: 'integer', + }, + 'fortinet.firewall.ha-prio': { + category: 'fortinet', + description: 'HA Priority ', + name: 'fortinet.firewall.ha-prio', + type: 'integer', + }, + 'fortinet.firewall.ha_group': { + category: 'fortinet', + description: 'HA Group ', + name: 'fortinet.firewall.ha_group', + type: 'keyword', + }, + 'fortinet.firewall.ha_role': { + category: 'fortinet', + description: 'HA Role ', + name: 'fortinet.firewall.ha_role', + type: 'keyword', + }, + 'fortinet.firewall.handshake': { + category: 'fortinet', + description: 'SSL Handshake ', + name: 'fortinet.firewall.handshake', + type: 'keyword', + }, + 'fortinet.firewall.hash': { + category: 'fortinet', + description: 'Hash value of downloaded file ', + name: 'fortinet.firewall.hash', + type: 'keyword', + }, + 'fortinet.firewall.hbdn_reason': { + category: 'fortinet', + description: 'Heartbeat down reason ', + name: 'fortinet.firewall.hbdn_reason', + type: 'keyword', + }, + 'fortinet.firewall.highcount': { + category: 'fortinet', + description: 'Highcount fabric summary ', + name: 'fortinet.firewall.highcount', + type: 'integer', + }, + 'fortinet.firewall.host': { + category: 'fortinet', + description: 'Hostname ', + name: 'fortinet.firewall.host', + type: 'keyword', + }, + 'fortinet.firewall.iaid': { + category: 'fortinet', + description: 'DHCPv6 id ', + name: 'fortinet.firewall.iaid', + type: 'keyword', + }, + 'fortinet.firewall.icmpcode': { + category: 'fortinet', + description: 'Destination Port of the ICMP message ', + name: 'fortinet.firewall.icmpcode', + type: 'keyword', + }, + 'fortinet.firewall.icmpid': { + category: 'fortinet', + description: 'Source port of the ICMP message ', + name: 'fortinet.firewall.icmpid', + type: 'keyword', + }, + 'fortinet.firewall.icmptype': { + category: 'fortinet', + description: 'The type of ICMP message ', + name: 'fortinet.firewall.icmptype', + type: 'keyword', + }, + 'fortinet.firewall.identifier': { + category: 'fortinet', + description: 'Network traffic identifier ', + name: 'fortinet.firewall.identifier', + type: 'integer', + }, + 'fortinet.firewall.in_spi': { + category: 'fortinet', + description: 'IPSEC inbound SPI ', + name: 'fortinet.firewall.in_spi', + type: 'keyword', + }, + 'fortinet.firewall.incidentserialno': { + category: 'fortinet', + description: 'Incident serial number ', + name: 'fortinet.firewall.incidentserialno', + type: 'integer', + }, + 'fortinet.firewall.infected': { + category: 'fortinet', + description: 'Infected MMS ', + name: 'fortinet.firewall.infected', + type: 'integer', + }, + 'fortinet.firewall.infectedfilelevel': { + category: 'fortinet', + description: 'DLP infected file level ', + name: 'fortinet.firewall.infectedfilelevel', + type: 'integer', + }, + 'fortinet.firewall.informationsource': { + category: 'fortinet', + description: 'Information source ', + name: 'fortinet.firewall.informationsource', + type: 'keyword', + }, + 'fortinet.firewall.init': { + category: 'fortinet', + description: 'IPSEC init stage ', + name: 'fortinet.firewall.init', + type: 'keyword', + }, + 'fortinet.firewall.initiator': { + category: 'fortinet', + description: 'Original login user name for Fortiguard override ', + name: 'fortinet.firewall.initiator', + type: 'keyword', + }, + 'fortinet.firewall.interface': { + category: 'fortinet', + description: 'Related interface ', + name: 'fortinet.firewall.interface', + type: 'keyword', + }, + 'fortinet.firewall.intf': { + category: 'fortinet', + description: 'Related interface ', + name: 'fortinet.firewall.intf', + type: 'keyword', + }, + 'fortinet.firewall.invalidmac': { + category: 'fortinet', + description: 'The MAC address with invalid OUI ', + name: 'fortinet.firewall.invalidmac', + type: 'keyword', + }, + 'fortinet.firewall.ip': { + category: 'fortinet', + description: 'Related IP ', + name: 'fortinet.firewall.ip', + type: 'ip', + }, + 'fortinet.firewall.iptype': { + category: 'fortinet', + description: 'Related IP type ', + name: 'fortinet.firewall.iptype', + type: 'keyword', + }, + 'fortinet.firewall.keyword': { + category: 'fortinet', + description: 'Keyword used for search ', + name: 'fortinet.firewall.keyword', + type: 'keyword', + }, + 'fortinet.firewall.kind': { + category: 'fortinet', + description: 'VOIP kind ', + name: 'fortinet.firewall.kind', + type: 'keyword', + }, + 'fortinet.firewall.lanin': { + category: 'fortinet', + description: 'LAN incoming traffic in bytes ', + name: 'fortinet.firewall.lanin', + type: 'long', + }, + 'fortinet.firewall.lanout': { + category: 'fortinet', + description: 'LAN outbound traffic in bytes ', + name: 'fortinet.firewall.lanout', + type: 'long', + }, + 'fortinet.firewall.lease': { + category: 'fortinet', + description: 'DHCP lease ', + name: 'fortinet.firewall.lease', + type: 'integer', + }, + 'fortinet.firewall.license_limit': { + category: 'fortinet', + description: 'Maximum Number of FortiClients for the License ', + name: 'fortinet.firewall.license_limit', + type: 'keyword', + }, + 'fortinet.firewall.limit': { + category: 'fortinet', + description: 'Virtual Domain Resource Limit ', + name: 'fortinet.firewall.limit', + type: 'integer', + }, + 'fortinet.firewall.line': { + category: 'fortinet', + description: 'VOIP line ', + name: 'fortinet.firewall.line', + type: 'keyword', + }, + 'fortinet.firewall.live': { + category: 'fortinet', + description: 'Time in seconds ', + name: 'fortinet.firewall.live', + type: 'integer', + }, + 'fortinet.firewall.local': { + category: 'fortinet', + description: 'Local IP for a PPPD Connection ', + name: 'fortinet.firewall.local', + type: 'ip', + }, + 'fortinet.firewall.log': { + category: 'fortinet', + description: 'Log message ', + name: 'fortinet.firewall.log', + type: 'keyword', + }, + 'fortinet.firewall.login': { + category: 'fortinet', + description: 'SSH login ', + name: 'fortinet.firewall.login', + type: 'keyword', + }, + 'fortinet.firewall.lowcount': { + category: 'fortinet', + description: 'Fabric lowcount ', + name: 'fortinet.firewall.lowcount', + type: 'integer', + }, + 'fortinet.firewall.mac': { + category: 'fortinet', + description: 'DHCP mac address ', + name: 'fortinet.firewall.mac', + type: 'keyword', + }, + 'fortinet.firewall.malform_data': { + category: 'fortinet', + description: 'VOIP malformed data ', + name: 'fortinet.firewall.malform_data', + type: 'integer', + }, + 'fortinet.firewall.malform_desc': { + category: 'fortinet', + description: 'VOIP malformed data description ', + name: 'fortinet.firewall.malform_desc', + type: 'keyword', + }, + 'fortinet.firewall.manuf': { + category: 'fortinet', + description: 'Manufacturer name ', + name: 'fortinet.firewall.manuf', + type: 'keyword', + }, + 'fortinet.firewall.masterdstmac': { + category: 'fortinet', + description: 'Master mac address for a host with multiple network interfaces ', + name: 'fortinet.firewall.masterdstmac', + type: 'keyword', + }, + 'fortinet.firewall.mastersrcmac': { + category: 'fortinet', + description: 'The master MAC address for a host that has multiple network interfaces ', + name: 'fortinet.firewall.mastersrcmac', + type: 'keyword', + }, + 'fortinet.firewall.mediumcount': { + category: 'fortinet', + description: 'Fabric medium count ', + name: 'fortinet.firewall.mediumcount', + type: 'integer', + }, + 'fortinet.firewall.mem': { + category: 'fortinet', + description: 'Memory usage system statistics ', + name: 'fortinet.firewall.mem', + type: 'keyword', + }, + 'fortinet.firewall.meshmode': { + category: 'fortinet', + description: 'Wireless mesh mode ', + name: 'fortinet.firewall.meshmode', + type: 'keyword', + }, + 'fortinet.firewall.message_type': { + category: 'fortinet', + description: 'VOIP message type ', + name: 'fortinet.firewall.message_type', + type: 'keyword', + }, + 'fortinet.firewall.method': { + category: 'fortinet', + description: 'HTTP method ', + name: 'fortinet.firewall.method', + type: 'keyword', + }, + 'fortinet.firewall.mgmtcnt': { + category: 'fortinet', + description: 'The number of unauthorized client flooding managemet frames ', + name: 'fortinet.firewall.mgmtcnt', + type: 'integer', + }, + 'fortinet.firewall.mode': { + category: 'fortinet', + description: 'IPSEC mode ', + name: 'fortinet.firewall.mode', + type: 'keyword', + }, + 'fortinet.firewall.module': { + category: 'fortinet', + description: 'PCI-DSS module ', + name: 'fortinet.firewall.module', + type: 'keyword', + }, + 'fortinet.firewall.monitor-name': { + category: 'fortinet', + description: 'Health Monitor Name ', + name: 'fortinet.firewall.monitor-name', + type: 'keyword', + }, + 'fortinet.firewall.monitor-type': { + category: 'fortinet', + description: 'Health Monitor Type ', + name: 'fortinet.firewall.monitor-type', + type: 'keyword', + }, + 'fortinet.firewall.mpsk': { + category: 'fortinet', + description: 'Wireless MPSK ', + name: 'fortinet.firewall.mpsk', + type: 'keyword', + }, + 'fortinet.firewall.msgproto': { + category: 'fortinet', + description: 'Message Protocol Number ', + name: 'fortinet.firewall.msgproto', + type: 'keyword', + }, + 'fortinet.firewall.mtu': { + category: 'fortinet', + description: 'Max Transmission Unit Value ', + name: 'fortinet.firewall.mtu', + type: 'integer', + }, + 'fortinet.firewall.name': { + category: 'fortinet', + description: 'Name ', + name: 'fortinet.firewall.name', + type: 'keyword', + }, + 'fortinet.firewall.nat': { + category: 'fortinet', + description: 'NAT IP Address ', + name: 'fortinet.firewall.nat', + type: 'keyword', + }, + 'fortinet.firewall.netid': { + category: 'fortinet', + description: 'Connector NetID ', + name: 'fortinet.firewall.netid', + type: 'keyword', + }, + 'fortinet.firewall.new_status': { + category: 'fortinet', + description: 'New status on user change ', + name: 'fortinet.firewall.new_status', + type: 'keyword', + }, + 'fortinet.firewall.new_value': { + category: 'fortinet', + description: 'New Virtual Domain Name ', + name: 'fortinet.firewall.new_value', + type: 'keyword', + }, + 'fortinet.firewall.newchannel': { + category: 'fortinet', + description: 'New Channel Number ', + name: 'fortinet.firewall.newchannel', + type: 'integer', + }, + 'fortinet.firewall.newchassisid': { + category: 'fortinet', + description: 'New Chassis ID ', + name: 'fortinet.firewall.newchassisid', + type: 'integer', + }, + 'fortinet.firewall.newslot': { + category: 'fortinet', + description: 'New Slot Number ', + name: 'fortinet.firewall.newslot', + type: 'integer', + }, + 'fortinet.firewall.nextstat': { + category: 'fortinet', + description: 'Time interval in seconds for the next statistics. ', + name: 'fortinet.firewall.nextstat', + type: 'integer', + }, + 'fortinet.firewall.nf_type': { + category: 'fortinet', + description: 'Notification Type ', + name: 'fortinet.firewall.nf_type', + type: 'keyword', + }, + 'fortinet.firewall.noise': { + category: 'fortinet', + description: 'Wifi Noise ', + name: 'fortinet.firewall.noise', + type: 'integer', + }, + 'fortinet.firewall.old_status': { + category: 'fortinet', + description: 'Original Status ', + name: 'fortinet.firewall.old_status', + type: 'keyword', + }, + 'fortinet.firewall.old_value': { + category: 'fortinet', + description: 'Original Virtual Domain name ', + name: 'fortinet.firewall.old_value', + type: 'keyword', + }, + 'fortinet.firewall.oldchannel': { + category: 'fortinet', + description: 'Original channel ', + name: 'fortinet.firewall.oldchannel', + type: 'integer', + }, + 'fortinet.firewall.oldchassisid': { + category: 'fortinet', + description: 'Original Chassis Number ', + name: 'fortinet.firewall.oldchassisid', + type: 'integer', + }, + 'fortinet.firewall.oldslot': { + category: 'fortinet', + description: 'Original Slot Number ', + name: 'fortinet.firewall.oldslot', + type: 'integer', + }, + 'fortinet.firewall.oldsn': { + category: 'fortinet', + description: 'Old Serial number ', + name: 'fortinet.firewall.oldsn', + type: 'keyword', + }, + 'fortinet.firewall.oldwprof': { + category: 'fortinet', + description: 'Old Web Filter Profile ', + name: 'fortinet.firewall.oldwprof', + type: 'keyword', + }, + 'fortinet.firewall.onwire': { + category: 'fortinet', + description: 'A flag to indicate if the AP is onwire or not ', + name: 'fortinet.firewall.onwire', + type: 'keyword', + }, + 'fortinet.firewall.opercountry': { + category: 'fortinet', + description: 'Operating Country ', + name: 'fortinet.firewall.opercountry', + type: 'keyword', + }, + 'fortinet.firewall.opertxpower': { + category: 'fortinet', + description: 'Operating TX power ', + name: 'fortinet.firewall.opertxpower', + type: 'integer', + }, + 'fortinet.firewall.osname': { + category: 'fortinet', + description: 'Operating System name ', + name: 'fortinet.firewall.osname', + type: 'keyword', + }, + 'fortinet.firewall.osversion': { + category: 'fortinet', + description: 'Operating System version ', + name: 'fortinet.firewall.osversion', + type: 'keyword', + }, + 'fortinet.firewall.out_spi': { + category: 'fortinet', + description: 'Out SPI ', + name: 'fortinet.firewall.out_spi', + type: 'keyword', + }, + 'fortinet.firewall.outintf': { + category: 'fortinet', + description: 'Out interface ', + name: 'fortinet.firewall.outintf', + type: 'keyword', + }, + 'fortinet.firewall.passedcount': { + category: 'fortinet', + description: 'Fabric passed count ', + name: 'fortinet.firewall.passedcount', + type: 'integer', + }, + 'fortinet.firewall.passwd': { + category: 'fortinet', + description: 'Changed user password information ', + name: 'fortinet.firewall.passwd', + type: 'keyword', + }, + 'fortinet.firewall.path': { + category: 'fortinet', + description: 'Path of looped configuration for security fabric ', + name: 'fortinet.firewall.path', + type: 'keyword', + }, + 'fortinet.firewall.peer': { + category: 'fortinet', + description: 'WAN optimization peer ', + name: 'fortinet.firewall.peer', + type: 'keyword', + }, + 'fortinet.firewall.peer_notif': { + category: 'fortinet', + description: 'VPN peer notification ', + name: 'fortinet.firewall.peer_notif', + type: 'keyword', + }, + 'fortinet.firewall.phase2_name': { + category: 'fortinet', + description: 'VPN phase2 name ', + name: 'fortinet.firewall.phase2_name', + type: 'keyword', + }, + 'fortinet.firewall.phone': { + category: 'fortinet', + description: 'VOIP Phone ', + name: 'fortinet.firewall.phone', + type: 'keyword', + }, + 'fortinet.firewall.pid': { + category: 'fortinet', + description: 'Process ID ', + name: 'fortinet.firewall.pid', + type: 'integer', + }, + 'fortinet.firewall.policytype': { + category: 'fortinet', + description: 'Policy Type ', + name: 'fortinet.firewall.policytype', + type: 'keyword', + }, + 'fortinet.firewall.poolname': { + category: 'fortinet', + description: 'IP Pool name ', + name: 'fortinet.firewall.poolname', + type: 'keyword', + }, + 'fortinet.firewall.port': { + category: 'fortinet', + description: 'Log upload error port ', + name: 'fortinet.firewall.port', + type: 'integer', + }, + 'fortinet.firewall.portbegin': { + category: 'fortinet', + description: 'IP Pool port number to begin ', + name: 'fortinet.firewall.portbegin', + type: 'integer', + }, + 'fortinet.firewall.portend': { + category: 'fortinet', + description: 'IP Pool port number to end ', + name: 'fortinet.firewall.portend', + type: 'integer', + }, + 'fortinet.firewall.probeproto': { + category: 'fortinet', + description: 'Link Monitor Probe Protocol ', + name: 'fortinet.firewall.probeproto', + type: 'keyword', + }, + 'fortinet.firewall.process': { + category: 'fortinet', + description: 'URL Filter process ', + name: 'fortinet.firewall.process', + type: 'keyword', + }, + 'fortinet.firewall.processtime': { + category: 'fortinet', + description: 'Process time for reports ', + name: 'fortinet.firewall.processtime', + type: 'integer', + }, + 'fortinet.firewall.profile': { + category: 'fortinet', + description: 'Profile Name ', + name: 'fortinet.firewall.profile', + type: 'keyword', + }, + 'fortinet.firewall.profile_vd': { + category: 'fortinet', + description: 'Virtual Domain Name ', + name: 'fortinet.firewall.profile_vd', + type: 'keyword', + }, + 'fortinet.firewall.profilegroup': { + category: 'fortinet', + description: 'Profile Group Name ', + name: 'fortinet.firewall.profilegroup', + type: 'keyword', + }, + 'fortinet.firewall.profiletype': { + category: 'fortinet', + description: 'Profile Type ', + name: 'fortinet.firewall.profiletype', + type: 'keyword', + }, + 'fortinet.firewall.qtypeval': { + category: 'fortinet', + description: 'DNS question type value ', + name: 'fortinet.firewall.qtypeval', + type: 'integer', + }, + 'fortinet.firewall.quarskip': { + category: 'fortinet', + description: 'Quarantine skip explanation ', + name: 'fortinet.firewall.quarskip', + type: 'keyword', + }, + 'fortinet.firewall.quotaexceeded': { + category: 'fortinet', + description: 'If quota has been exceeded ', + name: 'fortinet.firewall.quotaexceeded', + type: 'keyword', + }, + 'fortinet.firewall.quotamax': { + category: 'fortinet', + description: 'Maximum quota allowed - in seconds if time-based - in bytes if traffic-based ', + name: 'fortinet.firewall.quotamax', + type: 'long', + }, + 'fortinet.firewall.quotatype': { + category: 'fortinet', + description: 'Quota type ', + name: 'fortinet.firewall.quotatype', + type: 'keyword', + }, + 'fortinet.firewall.quotaused': { + category: 'fortinet', + description: 'Quota used - in seconds if time-based - in bytes if trafficbased) ', + name: 'fortinet.firewall.quotaused', + type: 'long', + }, + 'fortinet.firewall.radioband': { + category: 'fortinet', + description: 'Radio band ', + name: 'fortinet.firewall.radioband', + type: 'keyword', + }, + 'fortinet.firewall.radioid': { + category: 'fortinet', + description: 'Radio ID ', + name: 'fortinet.firewall.radioid', + type: 'integer', + }, + 'fortinet.firewall.radioidclosest': { + category: 'fortinet', + description: 'Radio ID on the AP closest the rogue AP ', + name: 'fortinet.firewall.radioidclosest', + type: 'integer', + }, + 'fortinet.firewall.radioiddetected': { + category: 'fortinet', + description: 'Radio ID on the AP which detected the rogue AP ', + name: 'fortinet.firewall.radioiddetected', + type: 'integer', + }, + 'fortinet.firewall.rate': { + category: 'fortinet', + description: 'Wireless rogue rate value ', + name: 'fortinet.firewall.rate', + type: 'keyword', + }, + 'fortinet.firewall.rawdata': { + category: 'fortinet', + description: 'Raw data value ', + name: 'fortinet.firewall.rawdata', + type: 'keyword', + }, + 'fortinet.firewall.rawdataid': { + category: 'fortinet', + description: 'Raw data ID ', + name: 'fortinet.firewall.rawdataid', + type: 'keyword', + }, + 'fortinet.firewall.rcvddelta': { + category: 'fortinet', + description: 'Received bytes delta ', + name: 'fortinet.firewall.rcvddelta', + type: 'keyword', + }, + 'fortinet.firewall.reason': { + category: 'fortinet', + description: 'Alert reason ', + name: 'fortinet.firewall.reason', + type: 'keyword', + }, + 'fortinet.firewall.received': { + category: 'fortinet', + description: 'Server key exchange received ', + name: 'fortinet.firewall.received', + type: 'integer', + }, + 'fortinet.firewall.receivedsignature': { + category: 'fortinet', + description: 'Server key exchange received signature ', + name: 'fortinet.firewall.receivedsignature', + type: 'keyword', + }, + 'fortinet.firewall.red': { + category: 'fortinet', + description: 'Memory information in red ', + name: 'fortinet.firewall.red', + type: 'keyword', + }, + 'fortinet.firewall.referralurl': { + category: 'fortinet', + description: 'Web filter referralurl ', + name: 'fortinet.firewall.referralurl', + type: 'keyword', + }, + 'fortinet.firewall.remote': { + category: 'fortinet', + description: 'Remote PPP IP address ', + name: 'fortinet.firewall.remote', + type: 'ip', + }, + 'fortinet.firewall.remotewtptime': { + category: 'fortinet', + description: 'Remote Wifi Radius authentication time ', + name: 'fortinet.firewall.remotewtptime', + type: 'keyword', + }, + 'fortinet.firewall.reporttype': { + category: 'fortinet', + description: 'Report type ', + name: 'fortinet.firewall.reporttype', + type: 'keyword', + }, + 'fortinet.firewall.reqtype': { + category: 'fortinet', + description: 'Request type ', + name: 'fortinet.firewall.reqtype', + type: 'keyword', + }, + 'fortinet.firewall.request_name': { + category: 'fortinet', + description: 'VOIP request name ', + name: 'fortinet.firewall.request_name', + type: 'keyword', + }, + 'fortinet.firewall.result': { + category: 'fortinet', + description: 'VPN phase result ', + name: 'fortinet.firewall.result', + type: 'keyword', + }, + 'fortinet.firewall.role': { + category: 'fortinet', + description: 'VPN Phase 2 role ', + name: 'fortinet.firewall.role', + type: 'keyword', + }, + 'fortinet.firewall.rssi': { + category: 'fortinet', + description: 'Received signal strength indicator ', + name: 'fortinet.firewall.rssi', + type: 'integer', + }, + 'fortinet.firewall.rsso_key': { + category: 'fortinet', + description: 'RADIUS SSO attribute value ', + name: 'fortinet.firewall.rsso_key', + type: 'keyword', + }, + 'fortinet.firewall.ruledata': { + category: 'fortinet', + description: 'Rule data ', + name: 'fortinet.firewall.ruledata', + type: 'keyword', + }, + 'fortinet.firewall.ruletype': { + category: 'fortinet', + description: 'Rule type ', + name: 'fortinet.firewall.ruletype', + type: 'keyword', + }, + 'fortinet.firewall.scanned': { + category: 'fortinet', + description: 'Number of Scanned MMSs ', + name: 'fortinet.firewall.scanned', + type: 'integer', + }, + 'fortinet.firewall.scantime': { + category: 'fortinet', + description: 'Scanned time ', + name: 'fortinet.firewall.scantime', + type: 'long', + }, + 'fortinet.firewall.scope': { + category: 'fortinet', + description: 'FortiGuard Override Scope ', + name: 'fortinet.firewall.scope', + type: 'keyword', + }, + 'fortinet.firewall.security': { + category: 'fortinet', + description: 'Wireless rogue security ', + name: 'fortinet.firewall.security', + type: 'keyword', + }, + 'fortinet.firewall.sensitivity': { + category: 'fortinet', + description: 'Sensitivity for document fingerprint ', + name: 'fortinet.firewall.sensitivity', + type: 'keyword', + }, + 'fortinet.firewall.sensor': { + category: 'fortinet', + description: 'NAC Sensor Name ', + name: 'fortinet.firewall.sensor', + type: 'keyword', + }, + 'fortinet.firewall.sentdelta': { + category: 'fortinet', + description: 'Sent bytes delta ', + name: 'fortinet.firewall.sentdelta', + type: 'keyword', + }, + 'fortinet.firewall.seq': { + category: 'fortinet', + description: 'Sequence number ', + name: 'fortinet.firewall.seq', + type: 'keyword', + }, + 'fortinet.firewall.serial': { + category: 'fortinet', + description: 'WAN optimisation serial ', + name: 'fortinet.firewall.serial', + type: 'keyword', + }, + 'fortinet.firewall.serialno': { + category: 'fortinet', + description: 'Serial number ', + name: 'fortinet.firewall.serialno', + type: 'keyword', + }, + 'fortinet.firewall.server': { + category: 'fortinet', + description: 'AD server FQDN or IP ', + name: 'fortinet.firewall.server', + type: 'keyword', + }, + 'fortinet.firewall.session_id': { + category: 'fortinet', + description: 'Session ID ', + name: 'fortinet.firewall.session_id', + type: 'keyword', + }, + 'fortinet.firewall.sessionid': { + category: 'fortinet', + description: 'WAD Session ID ', + name: 'fortinet.firewall.sessionid', + type: 'integer', + }, + 'fortinet.firewall.setuprate': { + category: 'fortinet', + description: 'Session Setup Rate ', + name: 'fortinet.firewall.setuprate', + type: 'long', + }, + 'fortinet.firewall.severity': { + category: 'fortinet', + description: 'Severity ', + name: 'fortinet.firewall.severity', + type: 'keyword', + }, + 'fortinet.firewall.shaperdroprcvdbyte': { + category: 'fortinet', + description: 'Received bytes dropped by shaper ', + name: 'fortinet.firewall.shaperdroprcvdbyte', + type: 'integer', + }, + 'fortinet.firewall.shaperdropsentbyte': { + category: 'fortinet', + description: 'Sent bytes dropped by shaper ', + name: 'fortinet.firewall.shaperdropsentbyte', + type: 'integer', + }, + 'fortinet.firewall.shaperperipdropbyte': { + category: 'fortinet', + description: 'Dropped bytes per IP by shaper ', + name: 'fortinet.firewall.shaperperipdropbyte', + type: 'integer', + }, + 'fortinet.firewall.shaperperipname': { + category: 'fortinet', + description: 'Traffic shaper name (per IP) ', + name: 'fortinet.firewall.shaperperipname', + type: 'keyword', + }, + 'fortinet.firewall.shaperrcvdname': { + category: 'fortinet', + description: 'Traffic shaper name for received traffic ', + name: 'fortinet.firewall.shaperrcvdname', + type: 'keyword', + }, + 'fortinet.firewall.shapersentname': { + category: 'fortinet', + description: 'Traffic shaper name for sent traffic ', + name: 'fortinet.firewall.shapersentname', + type: 'keyword', + }, + 'fortinet.firewall.shapingpolicyid': { + category: 'fortinet', + description: 'Traffic shaper policy ID ', + name: 'fortinet.firewall.shapingpolicyid', + type: 'integer', + }, + 'fortinet.firewall.signal': { + category: 'fortinet', + description: 'Wireless rogue API signal ', + name: 'fortinet.firewall.signal', + type: 'integer', + }, + 'fortinet.firewall.size': { + category: 'fortinet', + description: 'Email size in bytes ', + name: 'fortinet.firewall.size', + type: 'long', + }, + 'fortinet.firewall.slot': { + category: 'fortinet', + description: 'Slot number ', + name: 'fortinet.firewall.slot', + type: 'integer', + }, + 'fortinet.firewall.sn': { + category: 'fortinet', + description: 'Security fabric serial number ', + name: 'fortinet.firewall.sn', + type: 'keyword', + }, + 'fortinet.firewall.snclosest': { + category: 'fortinet', + description: 'SN of the AP closest to the rogue AP ', + name: 'fortinet.firewall.snclosest', + type: 'keyword', + }, + 'fortinet.firewall.sndetected': { + category: 'fortinet', + description: 'SN of the AP which detected the rogue AP ', + name: 'fortinet.firewall.sndetected', + type: 'keyword', + }, + 'fortinet.firewall.snmeshparent': { + category: 'fortinet', + description: 'SN of the mesh parent ', + name: 'fortinet.firewall.snmeshparent', + type: 'keyword', + }, + 'fortinet.firewall.spi': { + category: 'fortinet', + description: 'IPSEC SPI ', + name: 'fortinet.firewall.spi', + type: 'keyword', + }, + 'fortinet.firewall.src_int': { + category: 'fortinet', + description: 'Source interface ', + name: 'fortinet.firewall.src_int', + type: 'keyword', + }, + 'fortinet.firewall.srcintfrole': { + category: 'fortinet', + description: 'Source interface role ', + name: 'fortinet.firewall.srcintfrole', + type: 'keyword', + }, + 'fortinet.firewall.srccountry': { + category: 'fortinet', + description: 'Source country ', + name: 'fortinet.firewall.srccountry', + type: 'keyword', + }, + 'fortinet.firewall.srcfamily': { + category: 'fortinet', + description: 'Source family ', + name: 'fortinet.firewall.srcfamily', + type: 'keyword', + }, + 'fortinet.firewall.srchwvendor': { + category: 'fortinet', + description: 'Source hardware vendor ', + name: 'fortinet.firewall.srchwvendor', + type: 'keyword', + }, + 'fortinet.firewall.srchwversion': { + category: 'fortinet', + description: 'Source hardware version ', + name: 'fortinet.firewall.srchwversion', + type: 'keyword', + }, + 'fortinet.firewall.srcinetsvc': { + category: 'fortinet', + description: 'Source interface service ', + name: 'fortinet.firewall.srcinetsvc', + type: 'keyword', + }, + 'fortinet.firewall.srcname': { + category: 'fortinet', + description: 'Source name ', + name: 'fortinet.firewall.srcname', + type: 'keyword', + }, + 'fortinet.firewall.srcserver': { + category: 'fortinet', + description: 'Source server ', + name: 'fortinet.firewall.srcserver', + type: 'integer', + }, + 'fortinet.firewall.srcssid': { + category: 'fortinet', + description: 'Source SSID ', + name: 'fortinet.firewall.srcssid', + type: 'keyword', + }, + 'fortinet.firewall.srcswversion': { + category: 'fortinet', + description: 'Source software version ', + name: 'fortinet.firewall.srcswversion', + type: 'keyword', + }, + 'fortinet.firewall.srcuuid': { + category: 'fortinet', + description: 'Source UUID ', + name: 'fortinet.firewall.srcuuid', + type: 'keyword', + }, + 'fortinet.firewall.sscname': { + category: 'fortinet', + description: 'SSC name ', + name: 'fortinet.firewall.sscname', + type: 'keyword', + }, + 'fortinet.firewall.ssid': { + category: 'fortinet', + description: 'Base Service Set ID ', + name: 'fortinet.firewall.ssid', + type: 'keyword', + }, + 'fortinet.firewall.sslaction': { + category: 'fortinet', + description: 'SSL Action ', + name: 'fortinet.firewall.sslaction', + type: 'keyword', + }, + 'fortinet.firewall.ssllocal': { + category: 'fortinet', + description: 'WAD SSL local ', + name: 'fortinet.firewall.ssllocal', + type: 'keyword', + }, + 'fortinet.firewall.sslremote': { + category: 'fortinet', + description: 'WAD SSL remote ', + name: 'fortinet.firewall.sslremote', + type: 'keyword', + }, + 'fortinet.firewall.stacount': { + category: 'fortinet', + description: 'Number of stations/clients ', + name: 'fortinet.firewall.stacount', + type: 'integer', + }, + 'fortinet.firewall.stage': { + category: 'fortinet', + description: 'IPSEC stage ', + name: 'fortinet.firewall.stage', + type: 'keyword', + }, + 'fortinet.firewall.stamac': { + category: 'fortinet', + description: '802.1x station mac ', + name: 'fortinet.firewall.stamac', + type: 'keyword', + }, + 'fortinet.firewall.state': { + category: 'fortinet', + description: 'Admin login state ', + name: 'fortinet.firewall.state', + type: 'keyword', + }, + 'fortinet.firewall.status': { + category: 'fortinet', + description: 'Status ', + name: 'fortinet.firewall.status', + type: 'keyword', + }, + 'fortinet.firewall.stitch': { + category: 'fortinet', + description: 'Automation stitch triggered ', + name: 'fortinet.firewall.stitch', + type: 'keyword', + }, + 'fortinet.firewall.subject': { + category: 'fortinet', + description: 'Email subject ', + name: 'fortinet.firewall.subject', + type: 'keyword', + }, + 'fortinet.firewall.submodule': { + category: 'fortinet', + description: 'Configuration Sub-Module Name ', + name: 'fortinet.firewall.submodule', + type: 'keyword', + }, + 'fortinet.firewall.subservice': { + category: 'fortinet', + description: 'AV subservice ', + name: 'fortinet.firewall.subservice', + type: 'keyword', + }, + 'fortinet.firewall.subtype': { + category: 'fortinet', + description: 'Log subtype ', + name: 'fortinet.firewall.subtype', + type: 'keyword', + }, + 'fortinet.firewall.suspicious': { + category: 'fortinet', + description: 'Number of Suspicious MMSs ', + name: 'fortinet.firewall.suspicious', + type: 'integer', + }, + 'fortinet.firewall.switchproto': { + category: 'fortinet', + description: 'Protocol change information ', + name: 'fortinet.firewall.switchproto', + type: 'keyword', + }, + 'fortinet.firewall.sync_status': { + category: 'fortinet', + description: 'The sync status with the master ', + name: 'fortinet.firewall.sync_status', + type: 'keyword', + }, + 'fortinet.firewall.sync_type': { + category: 'fortinet', + description: 'The sync type with the master ', + name: 'fortinet.firewall.sync_type', + type: 'keyword', + }, + 'fortinet.firewall.sysuptime': { + category: 'fortinet', + description: 'System uptime ', + name: 'fortinet.firewall.sysuptime', + type: 'keyword', + }, + 'fortinet.firewall.tamac': { + category: 'fortinet', + description: 'the MAC address of Transmitter, if none, then Receiver ', + name: 'fortinet.firewall.tamac', + type: 'keyword', + }, + 'fortinet.firewall.threattype': { + category: 'fortinet', + description: 'WIDS threat type ', + name: 'fortinet.firewall.threattype', + type: 'keyword', + }, + 'fortinet.firewall.time': { + category: 'fortinet', + description: 'Time of the event ', + name: 'fortinet.firewall.time', + type: 'keyword', + }, + 'fortinet.firewall.to': { + category: 'fortinet', + description: 'Email to field ', + name: 'fortinet.firewall.to', + type: 'keyword', + }, + 'fortinet.firewall.to_vcluster': { + category: 'fortinet', + description: 'destination virtual cluster number ', + name: 'fortinet.firewall.to_vcluster', + type: 'integer', + }, + 'fortinet.firewall.total': { + category: 'fortinet', + description: 'Total memory ', + name: 'fortinet.firewall.total', + type: 'integer', + }, + 'fortinet.firewall.totalsession': { + category: 'fortinet', + description: 'Total Number of Sessions ', + name: 'fortinet.firewall.totalsession', + type: 'integer', + }, + 'fortinet.firewall.trace_id': { + category: 'fortinet', + description: 'Session clash trace ID ', + name: 'fortinet.firewall.trace_id', + type: 'keyword', + }, + 'fortinet.firewall.trandisp': { + category: 'fortinet', + description: 'NAT translation type ', + name: 'fortinet.firewall.trandisp', + type: 'keyword', + }, + 'fortinet.firewall.transid': { + category: 'fortinet', + description: 'HTTP transaction ID ', + name: 'fortinet.firewall.transid', + type: 'integer', + }, + 'fortinet.firewall.translationid': { + category: 'fortinet', + description: 'DNS filter transaltion ID ', + name: 'fortinet.firewall.translationid', + type: 'keyword', + }, + 'fortinet.firewall.trigger': { + category: 'fortinet', + description: 'Automation stitch trigger ', + name: 'fortinet.firewall.trigger', + type: 'keyword', + }, + 'fortinet.firewall.trueclntip': { + category: 'fortinet', + description: 'File filter true client IP ', + name: 'fortinet.firewall.trueclntip', + type: 'ip', + }, + 'fortinet.firewall.tunnelid': { + category: 'fortinet', + description: 'IPSEC tunnel ID ', + name: 'fortinet.firewall.tunnelid', + type: 'integer', + }, + 'fortinet.firewall.tunnelip': { + category: 'fortinet', + description: 'IPSEC tunnel IP ', + name: 'fortinet.firewall.tunnelip', + type: 'ip', + }, + 'fortinet.firewall.tunneltype': { + category: 'fortinet', + description: 'IPSEC tunnel type ', + name: 'fortinet.firewall.tunneltype', + type: 'keyword', + }, + 'fortinet.firewall.type': { + category: 'fortinet', + description: 'Module type ', + name: 'fortinet.firewall.type', + type: 'keyword', + }, + 'fortinet.firewall.ui': { + category: 'fortinet', + description: 'Admin authentication UI type ', + name: 'fortinet.firewall.ui', + type: 'keyword', + }, + 'fortinet.firewall.unauthusersource': { + category: 'fortinet', + description: 'Unauthenticated user source ', + name: 'fortinet.firewall.unauthusersource', + type: 'keyword', + }, + 'fortinet.firewall.unit': { + category: 'fortinet', + description: 'Power supply unit ', + name: 'fortinet.firewall.unit', + type: 'integer', + }, + 'fortinet.firewall.urlfilteridx': { + category: 'fortinet', + description: 'URL filter ID ', + name: 'fortinet.firewall.urlfilteridx', + type: 'integer', + }, + 'fortinet.firewall.urlfilterlist': { + category: 'fortinet', + description: 'URL filter list ', + name: 'fortinet.firewall.urlfilterlist', + type: 'keyword', + }, + 'fortinet.firewall.urlsource': { + category: 'fortinet', + description: 'URL filter source ', + name: 'fortinet.firewall.urlsource', + type: 'keyword', + }, + 'fortinet.firewall.urltype': { + category: 'fortinet', + description: 'URL filter type ', + name: 'fortinet.firewall.urltype', + type: 'keyword', + }, + 'fortinet.firewall.used': { + category: 'fortinet', + description: 'Number of Used IPs ', + name: 'fortinet.firewall.used', + type: 'integer', + }, + 'fortinet.firewall.used_for_type': { + category: 'fortinet', + description: 'Connection for the type ', + name: 'fortinet.firewall.used_for_type', + type: 'integer', + }, + 'fortinet.firewall.utmaction': { + category: 'fortinet', + description: 'Security action performed by UTM ', + name: 'fortinet.firewall.utmaction', + type: 'keyword', + }, + 'fortinet.firewall.vap': { + category: 'fortinet', + description: 'Virtual AP ', + name: 'fortinet.firewall.vap', + type: 'keyword', + }, + 'fortinet.firewall.vapmode': { + category: 'fortinet', + description: 'Virtual AP mode ', + name: 'fortinet.firewall.vapmode', + type: 'keyword', + }, + 'fortinet.firewall.vcluster': { + category: 'fortinet', + description: 'virtual cluster id ', + name: 'fortinet.firewall.vcluster', + type: 'integer', + }, + 'fortinet.firewall.vcluster_member': { + category: 'fortinet', + description: 'Virtual cluster member ', + name: 'fortinet.firewall.vcluster_member', + type: 'integer', + }, + 'fortinet.firewall.vcluster_state': { + category: 'fortinet', + description: 'Virtual cluster state ', + name: 'fortinet.firewall.vcluster_state', + type: 'keyword', + }, + 'fortinet.firewall.vd': { + category: 'fortinet', + description: 'Virtual Domain Name ', + name: 'fortinet.firewall.vd', + type: 'keyword', + }, + 'fortinet.firewall.vdname': { + category: 'fortinet', + description: 'Virtual Domain Name ', + name: 'fortinet.firewall.vdname', + type: 'keyword', + }, + 'fortinet.firewall.vendorurl': { + category: 'fortinet', + description: 'Vulnerability scan vendor name ', + name: 'fortinet.firewall.vendorurl', + type: 'keyword', + }, + 'fortinet.firewall.version': { + category: 'fortinet', + description: 'Version ', + name: 'fortinet.firewall.version', + type: 'keyword', + }, + 'fortinet.firewall.vip': { + category: 'fortinet', + description: 'Virtual IP ', + name: 'fortinet.firewall.vip', + type: 'keyword', + }, + 'fortinet.firewall.virus': { + category: 'fortinet', + description: 'Virus name ', + name: 'fortinet.firewall.virus', + type: 'keyword', + }, + 'fortinet.firewall.virusid': { + category: 'fortinet', + description: 'Virus ID (unique virus identifier) ', + name: 'fortinet.firewall.virusid', + type: 'integer', + }, + 'fortinet.firewall.voip_proto': { + category: 'fortinet', + description: 'VOIP protocol ', + name: 'fortinet.firewall.voip_proto', + type: 'keyword', + }, + 'fortinet.firewall.vpn': { + category: 'fortinet', + description: 'VPN description ', + name: 'fortinet.firewall.vpn', + type: 'keyword', + }, + 'fortinet.firewall.vpntunnel': { + category: 'fortinet', + description: 'IPsec Vpn Tunnel Name ', + name: 'fortinet.firewall.vpntunnel', + type: 'keyword', + }, + 'fortinet.firewall.vpntype': { + category: 'fortinet', + description: 'The type of the VPN tunnel ', + name: 'fortinet.firewall.vpntype', + type: 'keyword', + }, + 'fortinet.firewall.vrf': { + category: 'fortinet', + description: 'VRF number ', + name: 'fortinet.firewall.vrf', + type: 'integer', + }, + 'fortinet.firewall.vulncat': { + category: 'fortinet', + description: 'Vulnerability Category ', + name: 'fortinet.firewall.vulncat', + type: 'keyword', + }, + 'fortinet.firewall.vulnid': { + category: 'fortinet', + description: 'Vulnerability ID ', + name: 'fortinet.firewall.vulnid', + type: 'integer', + }, + 'fortinet.firewall.vulnname': { + category: 'fortinet', + description: 'Vulnerability name ', + name: 'fortinet.firewall.vulnname', + type: 'keyword', + }, + 'fortinet.firewall.vwlid': { + category: 'fortinet', + description: 'VWL ID ', + name: 'fortinet.firewall.vwlid', + type: 'integer', + }, + 'fortinet.firewall.vwlquality': { + category: 'fortinet', + description: 'VWL quality ', + name: 'fortinet.firewall.vwlquality', + type: 'keyword', + }, + 'fortinet.firewall.vwlservice': { + category: 'fortinet', + description: 'VWL service ', + name: 'fortinet.firewall.vwlservice', + type: 'keyword', + }, + 'fortinet.firewall.vwpvlanid': { + category: 'fortinet', + description: 'VWP VLAN ID ', + name: 'fortinet.firewall.vwpvlanid', + type: 'integer', + }, + 'fortinet.firewall.wanin': { + category: 'fortinet', + description: 'WAN incoming traffic in bytes ', + name: 'fortinet.firewall.wanin', + type: 'long', + }, + 'fortinet.firewall.wanoptapptype': { + category: 'fortinet', + description: 'WAN Optimization Application type ', + name: 'fortinet.firewall.wanoptapptype', + type: 'keyword', + }, + 'fortinet.firewall.wanout': { + category: 'fortinet', + description: 'WAN outgoing traffic in bytes ', + name: 'fortinet.firewall.wanout', + type: 'long', + }, + 'fortinet.firewall.weakwepiv': { + category: 'fortinet', + description: 'Weak Wep Initiation Vector ', + name: 'fortinet.firewall.weakwepiv', + type: 'keyword', + }, + 'fortinet.firewall.xauthgroup': { + category: 'fortinet', + description: 'XAuth Group Name ', + name: 'fortinet.firewall.xauthgroup', + type: 'keyword', + }, + 'fortinet.firewall.xauthuser': { + category: 'fortinet', + description: 'XAuth User Name ', + name: 'fortinet.firewall.xauthuser', + type: 'keyword', + }, + 'fortinet.firewall.xid': { + category: 'fortinet', + description: 'Wireless X ID ', + name: 'fortinet.firewall.xid', + type: 'integer', + }, + 'googlecloud.destination.instance.project_id': { + category: 'googlecloud', + description: 'ID of the project containing the VM. ', + name: 'googlecloud.destination.instance.project_id', + type: 'keyword', + }, + 'googlecloud.destination.instance.region': { + category: 'googlecloud', + description: 'Region of the VM. ', + name: 'googlecloud.destination.instance.region', + type: 'keyword', + }, + 'googlecloud.destination.instance.zone': { + category: 'googlecloud', + description: 'Zone of the VM. ', + name: 'googlecloud.destination.instance.zone', + type: 'keyword', + }, + 'googlecloud.destination.vpc.project_id': { + category: 'googlecloud', + description: 'ID of the project containing the VM. ', + name: 'googlecloud.destination.vpc.project_id', + type: 'keyword', + }, + 'googlecloud.destination.vpc.vpc_name': { + category: 'googlecloud', + description: 'VPC on which the VM is operating. ', + name: 'googlecloud.destination.vpc.vpc_name', + type: 'keyword', + }, + 'googlecloud.destination.vpc.subnetwork_name': { + category: 'googlecloud', + description: 'Subnetwork on which the VM is operating. ', + name: 'googlecloud.destination.vpc.subnetwork_name', + type: 'keyword', + }, + 'googlecloud.source.instance.project_id': { + category: 'googlecloud', + description: 'ID of the project containing the VM. ', + name: 'googlecloud.source.instance.project_id', + type: 'keyword', + }, + 'googlecloud.source.instance.region': { + category: 'googlecloud', + description: 'Region of the VM. ', + name: 'googlecloud.source.instance.region', + type: 'keyword', + }, + 'googlecloud.source.instance.zone': { + category: 'googlecloud', + description: 'Zone of the VM. ', + name: 'googlecloud.source.instance.zone', + type: 'keyword', + }, + 'googlecloud.source.vpc.project_id': { + category: 'googlecloud', + description: 'ID of the project containing the VM. ', + name: 'googlecloud.source.vpc.project_id', + type: 'keyword', + }, + 'googlecloud.source.vpc.vpc_name': { + category: 'googlecloud', + description: 'VPC on which the VM is operating. ', + name: 'googlecloud.source.vpc.vpc_name', + type: 'keyword', + }, + 'googlecloud.source.vpc.subnetwork_name': { + category: 'googlecloud', + description: 'Subnetwork on which the VM is operating. ', + name: 'googlecloud.source.vpc.subnetwork_name', + type: 'keyword', + }, + 'googlecloud.audit.type': { + category: 'googlecloud', + description: 'Type property. ', + name: 'googlecloud.audit.type', + type: 'keyword', + }, + 'googlecloud.audit.authentication_info.principal_email': { + category: 'googlecloud', + description: 'The email address of the authenticated user making the request. ', + name: 'googlecloud.audit.authentication_info.principal_email', + type: 'keyword', + }, + 'googlecloud.audit.authentication_info.authority_selector': { + category: 'googlecloud', + description: + 'The authority selector specified by the requestor, if any. It is not guaranteed that the principal was allowed to use this authority. ', + name: 'googlecloud.audit.authentication_info.authority_selector', + type: 'keyword', + }, + 'googlecloud.audit.authorization_info.permission': { + category: 'googlecloud', + description: 'The required IAM permission. ', + name: 'googlecloud.audit.authorization_info.permission', + type: 'keyword', + }, + 'googlecloud.audit.authorization_info.granted': { + category: 'googlecloud', + description: 'Whether or not authorization for resource and permission was granted. ', + name: 'googlecloud.audit.authorization_info.granted', + type: 'boolean', + }, + 'googlecloud.audit.authorization_info.resource_attributes.service': { + category: 'googlecloud', + description: 'The name of the service. ', + name: 'googlecloud.audit.authorization_info.resource_attributes.service', + type: 'keyword', + }, + 'googlecloud.audit.authorization_info.resource_attributes.name': { + category: 'googlecloud', + description: 'The name of the resource. ', + name: 'googlecloud.audit.authorization_info.resource_attributes.name', + type: 'keyword', + }, + 'googlecloud.audit.authorization_info.resource_attributes.type': { + category: 'googlecloud', + description: 'The type of the resource. ', + name: 'googlecloud.audit.authorization_info.resource_attributes.type', + type: 'keyword', + }, + 'googlecloud.audit.method_name': { + category: 'googlecloud', + description: + "The name of the service method or operation. For API calls, this should be the name of the API method. For example, 'google.datastore.v1.Datastore.RunQuery'. ", + name: 'googlecloud.audit.method_name', + type: 'keyword', + }, + 'googlecloud.audit.num_response_items': { + category: 'googlecloud', + description: 'The number of items returned from a List or Query API method, if applicable. ', + name: 'googlecloud.audit.num_response_items', + type: 'long', + }, + 'googlecloud.audit.request.proto_name': { + category: 'googlecloud', + description: 'Type property of the request. ', + name: 'googlecloud.audit.request.proto_name', + type: 'keyword', + }, + 'googlecloud.audit.request.filter': { + category: 'googlecloud', + description: 'Filter of the request. ', + name: 'googlecloud.audit.request.filter', + type: 'keyword', + }, + 'googlecloud.audit.request.name': { + category: 'googlecloud', + description: 'Name of the request. ', + name: 'googlecloud.audit.request.name', + type: 'keyword', + }, + 'googlecloud.audit.request.resource_name': { + category: 'googlecloud', + description: 'Name of the request resource. ', + name: 'googlecloud.audit.request.resource_name', + type: 'keyword', + }, + 'googlecloud.audit.request_metadata.caller_ip': { + category: 'googlecloud', + description: 'The IP address of the caller. ', + name: 'googlecloud.audit.request_metadata.caller_ip', + type: 'ip', + }, + 'googlecloud.audit.request_metadata.caller_supplied_user_agent': { + category: 'googlecloud', + description: + 'The user agent of the caller. This information is not authenticated and should be treated accordingly. ', + name: 'googlecloud.audit.request_metadata.caller_supplied_user_agent', + type: 'keyword', + }, + 'googlecloud.audit.response.proto_name': { + category: 'googlecloud', + description: 'Type property of the response. ', + name: 'googlecloud.audit.response.proto_name', + type: 'keyword', + }, + 'googlecloud.audit.response.details.group': { + category: 'googlecloud', + description: 'The name of the group. ', + name: 'googlecloud.audit.response.details.group', + type: 'keyword', + }, + 'googlecloud.audit.response.details.kind': { + category: 'googlecloud', + description: 'The kind of the response details. ', + name: 'googlecloud.audit.response.details.kind', + type: 'keyword', + }, + 'googlecloud.audit.response.details.name': { + category: 'googlecloud', + description: 'The name of the response details. ', + name: 'googlecloud.audit.response.details.name', + type: 'keyword', + }, + 'googlecloud.audit.response.details.uid': { + category: 'googlecloud', + description: 'The uid of the response details. ', + name: 'googlecloud.audit.response.details.uid', + type: 'keyword', + }, + 'googlecloud.audit.response.status': { + category: 'googlecloud', + description: 'Status of the response. ', + name: 'googlecloud.audit.response.status', + type: 'keyword', + }, + 'googlecloud.audit.resource_name': { + category: 'googlecloud', + description: + "The resource or collection that is the target of the operation. The name is a scheme-less URI, not including the API service name. For example, 'shelves/SHELF_ID/books'. ", + name: 'googlecloud.audit.resource_name', + type: 'keyword', + }, + 'googlecloud.audit.resource_location.current_locations': { + category: 'googlecloud', + description: 'Current locations of the resource. ', + name: 'googlecloud.audit.resource_location.current_locations', + type: 'keyword', + }, + 'googlecloud.audit.service_name': { + category: 'googlecloud', + description: + 'The name of the API service performing the operation. For example, datastore.googleapis.com. ', + name: 'googlecloud.audit.service_name', + type: 'keyword', + }, + 'googlecloud.audit.status.code': { + category: 'googlecloud', + description: 'The status code, which should be an enum value of google.rpc.Code. ', + name: 'googlecloud.audit.status.code', + type: 'integer', + }, + 'googlecloud.audit.status.message': { + category: 'googlecloud', + description: + 'A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the google.rpc.Status.details field, or localized by the client. ', + name: 'googlecloud.audit.status.message', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.priority': { + category: 'googlecloud', + description: 'The priority for the firewall rule.', + name: 'googlecloud.firewall.rule_details.priority', + type: 'long', + }, + 'googlecloud.firewall.rule_details.action': { + category: 'googlecloud', + description: 'Action that the rule performs on match.', + name: 'googlecloud.firewall.rule_details.action', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.direction': { + category: 'googlecloud', + description: 'Direction of traffic that matches this rule.', + name: 'googlecloud.firewall.rule_details.direction', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.reference': { + category: 'googlecloud', + description: 'Reference to the firewall rule.', + name: 'googlecloud.firewall.rule_details.reference', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.source_range': { + category: 'googlecloud', + description: 'List of source ranges that the firewall rule applies to.', + name: 'googlecloud.firewall.rule_details.source_range', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.destination_range': { + category: 'googlecloud', + description: 'List of destination ranges that the firewall applies to.', + name: 'googlecloud.firewall.rule_details.destination_range', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.source_tag': { + category: 'googlecloud', + description: 'List of all the source tags that the firewall rule applies to. ', + name: 'googlecloud.firewall.rule_details.source_tag', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.target_tag': { + category: 'googlecloud', + description: 'List of all the target tags that the firewall rule applies to. ', + name: 'googlecloud.firewall.rule_details.target_tag', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.ip_port_info': { + category: 'googlecloud', + description: 'List of ip protocols and applicable port ranges for rules. ', + name: 'googlecloud.firewall.rule_details.ip_port_info', + type: 'array', + }, + 'googlecloud.firewall.rule_details.source_service_account': { + category: 'googlecloud', + description: 'List of all the source service accounts that the firewall rule applies to. ', + name: 'googlecloud.firewall.rule_details.source_service_account', + type: 'keyword', + }, + 'googlecloud.firewall.rule_details.target_service_account': { + category: 'googlecloud', + description: 'List of all the target service accounts that the firewall rule applies to. ', + name: 'googlecloud.firewall.rule_details.target_service_account', + type: 'keyword', + }, + 'googlecloud.vpcflow.reporter': { + category: 'googlecloud', + description: "The side which reported the flow. Can be either 'SRC' or 'DEST'. ", + name: 'googlecloud.vpcflow.reporter', + type: 'keyword', + }, + 'googlecloud.vpcflow.rtt.ms': { + category: 'googlecloud', + description: + 'Latency as measured (for TCP flows only) during the time interval. This is the time elapsed between sending a SEQ and receiving a corresponding ACK and it contains the network RTT as well as the application related delay. ', + name: 'googlecloud.vpcflow.rtt.ms', + type: 'long', + }, + 'gsuite.actor.type': { + category: 'gsuite', + description: + 'The type of actor. Values can be: *USER*: Another user in the same domain. *EXTERNAL_USER*: A user outside the domain. *KEY*: A non-human actor. ', + name: 'gsuite.actor.type', + type: 'keyword', + }, + 'gsuite.actor.key': { + category: 'gsuite', + description: + 'Only present when `actor.type` is `KEY`. Can be the `consumer_key` of the requestor for OAuth 2LO API requests or an identifier for robot accounts. ', + name: 'gsuite.actor.key', + type: 'keyword', + }, + 'gsuite.event.type': { + category: 'gsuite', + description: + 'The type of GSuite event, mapped from `items[].events[].type` in the original payload. Each fileset can have a different set of values for it, more details can be found at https://developers.google.com/admin-sdk/reports/v1/reference/activities/list ', + example: 'audit#activity', + name: 'gsuite.event.type', + type: 'keyword', + }, + 'gsuite.kind': { + category: 'gsuite', + description: + 'The type of API resource, mapped from `kind` in the original payload. More details can be found at https://developers.google.com/admin-sdk/reports/v1/reference/activities/list ', + example: 'audit#activity', + name: 'gsuite.kind', + type: 'keyword', + }, + 'gsuite.organization.domain': { + category: 'gsuite', + description: "The domain that is affected by the report's event. ", + name: 'gsuite.organization.domain', + type: 'keyword', + }, + 'gsuite.admin.application.edition': { + category: 'gsuite', + description: 'The GSuite edition.', + name: 'gsuite.admin.application.edition', + type: 'keyword', + }, + 'gsuite.admin.application.name': { + category: 'gsuite', + description: "The application's name.", + name: 'gsuite.admin.application.name', + type: 'keyword', + }, + 'gsuite.admin.application.enabled': { + category: 'gsuite', + description: 'The enabled application.', + name: 'gsuite.admin.application.enabled', + type: 'keyword', + }, + 'gsuite.admin.application.licences_order_number': { + category: 'gsuite', + description: 'Order number used to redeem licenses.', + name: 'gsuite.admin.application.licences_order_number', + type: 'keyword', + }, + 'gsuite.admin.application.licences_purchased': { + category: 'gsuite', + description: 'Number of licences purchased.', + name: 'gsuite.admin.application.licences_purchased', + type: 'keyword', + }, + 'gsuite.admin.application.id': { + category: 'gsuite', + description: 'The application ID.', + name: 'gsuite.admin.application.id', + type: 'keyword', + }, + 'gsuite.admin.application.asp_id': { + category: 'gsuite', + description: 'The application specific password ID.', + name: 'gsuite.admin.application.asp_id', + type: 'keyword', + }, + 'gsuite.admin.application.package_id': { + category: 'gsuite', + description: 'The mobile application package ID.', + name: 'gsuite.admin.application.package_id', + type: 'keyword', + }, + 'gsuite.admin.group.email': { + category: 'gsuite', + description: "The group's primary email address.", + name: 'gsuite.admin.group.email', + type: 'keyword', + }, + 'gsuite.admin.new_value': { + category: 'gsuite', + description: 'The new value for the setting.', + name: 'gsuite.admin.new_value', + type: 'keyword', + }, + 'gsuite.admin.old_value': { + category: 'gsuite', + description: 'The old value for the setting.', + name: 'gsuite.admin.old_value', + type: 'keyword', + }, + 'gsuite.admin.org_unit.name': { + category: 'gsuite', + description: 'The organizational unit name.', + name: 'gsuite.admin.org_unit.name', + type: 'keyword', + }, + 'gsuite.admin.org_unit.full': { + category: 'gsuite', + description: 'The org unit full path including the root org unit name.', + name: 'gsuite.admin.org_unit.full', + type: 'keyword', + }, + 'gsuite.admin.setting.name': { + category: 'gsuite', + description: 'The setting name.', + name: 'gsuite.admin.setting.name', + type: 'keyword', + }, + 'gsuite.admin.user_defined_setting.name': { + category: 'gsuite', + description: 'The name of the user-defined setting.', + name: 'gsuite.admin.user_defined_setting.name', + type: 'keyword', + }, + 'gsuite.admin.setting.description': { + category: 'gsuite', + description: 'The setting name.', + name: 'gsuite.admin.setting.description', + type: 'keyword', + }, + 'gsuite.admin.group.priorities': { + category: 'gsuite', + description: 'Group priorities.', + name: 'gsuite.admin.group.priorities', + type: 'keyword', + }, + 'gsuite.admin.domain.alias': { + category: 'gsuite', + description: 'The domain alias.', + name: 'gsuite.admin.domain.alias', + type: 'keyword', + }, + 'gsuite.admin.domain.name': { + category: 'gsuite', + description: 'The primary domain name.', + name: 'gsuite.admin.domain.name', + type: 'keyword', + }, + 'gsuite.admin.domain.secondary_name': { + category: 'gsuite', + description: 'The secondary domain name.', + name: 'gsuite.admin.domain.secondary_name', + type: 'keyword', + }, + 'gsuite.admin.managed_configuration': { + category: 'gsuite', + description: 'The name of the managed configuration.', + name: 'gsuite.admin.managed_configuration', + type: 'keyword', + }, + 'gsuite.admin.non_featured_services_selection': { + category: 'gsuite', + description: + 'Non-featured services selection. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-application-settings#FLASHLIGHT_EDU_NON_FEATURED_SERVICES_SELECTED ', + name: 'gsuite.admin.non_featured_services_selection', + type: 'keyword', + }, + 'gsuite.admin.field': { + category: 'gsuite', + description: 'The name of the field.', + name: 'gsuite.admin.field', + type: 'keyword', + }, + 'gsuite.admin.resource.id': { + category: 'gsuite', + description: 'The name of the resource identifier.', + name: 'gsuite.admin.resource.id', + type: 'keyword', + }, + 'gsuite.admin.user.email': { + category: 'gsuite', + description: "The user's primary email address.", + name: 'gsuite.admin.user.email', + type: 'keyword', + }, + 'gsuite.admin.user.nickname': { + category: 'gsuite', + description: "The user's nickname.", + name: 'gsuite.admin.user.nickname', + type: 'keyword', + }, + 'gsuite.admin.user.birthdate': { + category: 'gsuite', + description: "The user's birth date.", + name: 'gsuite.admin.user.birthdate', + type: 'date', + }, + 'gsuite.admin.gateway.name': { + category: 'gsuite', + description: 'Gateway name. Present on some chat settings.', + name: 'gsuite.admin.gateway.name', + type: 'keyword', + }, + 'gsuite.admin.chrome_os.session_type': { + category: 'gsuite', + description: 'Chrome OS session type.', + name: 'gsuite.admin.chrome_os.session_type', + type: 'keyword', + }, + 'gsuite.admin.device.serial_number': { + category: 'gsuite', + description: 'Device serial number.', + name: 'gsuite.admin.device.serial_number', + type: 'keyword', + }, + 'gsuite.admin.device.id': { + category: 'gsuite', + name: 'gsuite.admin.device.id', + type: 'keyword', + }, + 'gsuite.admin.device.type': { + category: 'gsuite', + description: 'Device type.', + name: 'gsuite.admin.device.type', + type: 'keyword', + }, + 'gsuite.admin.print_server.name': { + category: 'gsuite', + description: 'The name of the print server.', + name: 'gsuite.admin.print_server.name', + type: 'keyword', + }, + 'gsuite.admin.printer.name': { + category: 'gsuite', + description: 'The name of the printer.', + name: 'gsuite.admin.printer.name', + type: 'keyword', + }, + 'gsuite.admin.device.command_details': { + category: 'gsuite', + description: 'Command details.', + name: 'gsuite.admin.device.command_details', + type: 'keyword', + }, + 'gsuite.admin.role.id': { + category: 'gsuite', + description: 'Unique identifier for this role privilege.', + name: 'gsuite.admin.role.id', + type: 'keyword', + }, + 'gsuite.admin.role.name': { + category: 'gsuite', + description: + 'The role name. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-delegated-admin-settings ', + name: 'gsuite.admin.role.name', + type: 'keyword', + }, + 'gsuite.admin.privilege.name': { + category: 'gsuite', + description: 'Privilege name.', + name: 'gsuite.admin.privilege.name', + type: 'keyword', + }, + 'gsuite.admin.service.name': { + category: 'gsuite', + description: 'The service name.', + name: 'gsuite.admin.service.name', + type: 'keyword', + }, + 'gsuite.admin.url.name': { + category: 'gsuite', + description: 'The website name.', + name: 'gsuite.admin.url.name', + type: 'keyword', + }, + 'gsuite.admin.product.name': { + category: 'gsuite', + description: 'The product name.', + name: 'gsuite.admin.product.name', + type: 'keyword', + }, + 'gsuite.admin.product.sku': { + category: 'gsuite', + description: 'The product SKU.', + name: 'gsuite.admin.product.sku', + type: 'keyword', + }, + 'gsuite.admin.bulk_upload.failed': { + category: 'gsuite', + description: 'Number of failed records in bulk upload operation.', + name: 'gsuite.admin.bulk_upload.failed', + type: 'long', + }, + 'gsuite.admin.bulk_upload.total': { + category: 'gsuite', + description: 'Number of total records in bulk upload operation.', + name: 'gsuite.admin.bulk_upload.total', + type: 'long', + }, + 'gsuite.admin.group.allowed_list': { + category: 'gsuite', + description: 'Names of allow-listed groups.', + name: 'gsuite.admin.group.allowed_list', + type: 'keyword', + }, + 'gsuite.admin.email.quarantine_name': { + category: 'gsuite', + description: 'The name of the quarantine.', + name: 'gsuite.admin.email.quarantine_name', + type: 'keyword', + }, + 'gsuite.admin.email.log_search_filter.message_id': { + category: 'gsuite', + description: "The log search filter's email message ID.", + name: 'gsuite.admin.email.log_search_filter.message_id', + type: 'keyword', + }, + 'gsuite.admin.email.log_search_filter.start_date': { + category: 'gsuite', + description: "The log search filter's start date.", + name: 'gsuite.admin.email.log_search_filter.start_date', + type: 'date', + }, + 'gsuite.admin.email.log_search_filter.end_date': { + category: 'gsuite', + description: "The log search filter's ending date.", + name: 'gsuite.admin.email.log_search_filter.end_date', + type: 'date', + }, + 'gsuite.admin.email.log_search_filter.recipient.value': { + category: 'gsuite', + description: "The log search filter's email recipient.", + name: 'gsuite.admin.email.log_search_filter.recipient.value', + type: 'keyword', + }, + 'gsuite.admin.email.log_search_filter.sender.value': { + category: 'gsuite', + description: "The log search filter's email sender.", + name: 'gsuite.admin.email.log_search_filter.sender.value', + type: 'keyword', + }, + 'gsuite.admin.email.log_search_filter.recipient.ip': { + category: 'gsuite', + description: "The log search filter's email recipient's IP address.", + name: 'gsuite.admin.email.log_search_filter.recipient.ip', + type: 'ip', + }, + 'gsuite.admin.email.log_search_filter.sender.ip': { + category: 'gsuite', + description: "The log search filter's email sender's IP address.", + name: 'gsuite.admin.email.log_search_filter.sender.ip', + type: 'ip', + }, + 'gsuite.admin.chrome_licenses.enabled': { + category: 'gsuite', + description: + 'Licences enabled. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-org-settings ', + name: 'gsuite.admin.chrome_licenses.enabled', + type: 'keyword', + }, + 'gsuite.admin.chrome_licenses.allowed': { + category: 'gsuite', + description: + 'Licences enabled. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-org-settings ', + name: 'gsuite.admin.chrome_licenses.allowed', + type: 'keyword', + }, + 'gsuite.admin.oauth2.service.name': { + category: 'gsuite', + description: + 'OAuth2 service name. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-security-settings ', + name: 'gsuite.admin.oauth2.service.name', + type: 'keyword', + }, + 'gsuite.admin.oauth2.application.id': { + category: 'gsuite', + description: 'OAuth2 application ID.', + name: 'gsuite.admin.oauth2.application.id', + type: 'keyword', + }, + 'gsuite.admin.oauth2.application.name': { + category: 'gsuite', + description: 'OAuth2 application name.', + name: 'gsuite.admin.oauth2.application.name', + type: 'keyword', + }, + 'gsuite.admin.oauth2.application.type': { + category: 'gsuite', + description: + 'OAuth2 application type. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-security-settings ', + name: 'gsuite.admin.oauth2.application.type', + type: 'keyword', + }, + 'gsuite.admin.verification_method': { + category: 'gsuite', + description: + 'Related verification method. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-security-settings and https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-domain-settings ', + name: 'gsuite.admin.verification_method', + type: 'keyword', + }, + 'gsuite.admin.alert.name': { + category: 'gsuite', + description: 'The alert name.', + name: 'gsuite.admin.alert.name', + type: 'keyword', + }, + 'gsuite.admin.rule.name': { + category: 'gsuite', + description: 'The rule name.', + name: 'gsuite.admin.rule.name', + type: 'keyword', + }, + 'gsuite.admin.api.client.name': { + category: 'gsuite', + description: 'The API client name.', + name: 'gsuite.admin.api.client.name', + type: 'keyword', + }, + 'gsuite.admin.api.scopes': { + category: 'gsuite', + description: 'The API scopes.', + name: 'gsuite.admin.api.scopes', + type: 'keyword', + }, + 'gsuite.admin.mdm.token': { + category: 'gsuite', + description: 'The MDM vendor enrollment token.', + name: 'gsuite.admin.mdm.token', + type: 'keyword', + }, + 'gsuite.admin.mdm.vendor': { + category: 'gsuite', + description: "The MDM vendor's name.", + name: 'gsuite.admin.mdm.vendor', + type: 'keyword', + }, + 'gsuite.admin.info_type': { + category: 'gsuite', + description: + 'This will be used to state what kind of information was changed. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-domain-settings ', + name: 'gsuite.admin.info_type', + type: 'keyword', + }, + 'gsuite.admin.email_monitor.dest_email': { + category: 'gsuite', + description: 'The destination address of the email monitor.', + name: 'gsuite.admin.email_monitor.dest_email', + type: 'keyword', + }, + 'gsuite.admin.email_monitor.level.chat': { + category: 'gsuite', + description: 'The chat email monitor level.', + name: 'gsuite.admin.email_monitor.level.chat', + type: 'keyword', + }, + 'gsuite.admin.email_monitor.level.draft': { + category: 'gsuite', + description: 'The draft email monitor level.', + name: 'gsuite.admin.email_monitor.level.draft', + type: 'keyword', + }, + 'gsuite.admin.email_monitor.level.incoming': { + category: 'gsuite', + description: 'The incoming email monitor level.', + name: 'gsuite.admin.email_monitor.level.incoming', + type: 'keyword', + }, + 'gsuite.admin.email_monitor.level.outgoing': { + category: 'gsuite', + description: 'The outgoing email monitor level.', + name: 'gsuite.admin.email_monitor.level.outgoing', + type: 'keyword', + }, + 'gsuite.admin.email_dump.include_deleted': { + category: 'gsuite', + description: 'Indicates if deleted emails are included in the export.', + name: 'gsuite.admin.email_dump.include_deleted', + type: 'boolean', + }, + 'gsuite.admin.email_dump.package_content': { + category: 'gsuite', + description: 'The contents of the mailbox package.', + name: 'gsuite.admin.email_dump.package_content', + type: 'keyword', + }, + 'gsuite.admin.email_dump.query': { + category: 'gsuite', + description: 'The search query used for the dump.', + name: 'gsuite.admin.email_dump.query', + type: 'keyword', + }, + 'gsuite.admin.request.id': { + category: 'gsuite', + description: 'The request ID.', + name: 'gsuite.admin.request.id', + type: 'keyword', + }, + 'gsuite.admin.mobile.action.id': { + category: 'gsuite', + description: "The mobile device action's ID.", + name: 'gsuite.admin.mobile.action.id', + type: 'keyword', + }, + 'gsuite.admin.mobile.action.type': { + category: 'gsuite', + description: + "The mobile device action's type. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-mobile-settings ", + name: 'gsuite.admin.mobile.action.type', + type: 'keyword', + }, + 'gsuite.admin.mobile.certificate.name': { + category: 'gsuite', + description: 'The mobile certificate common name.', + name: 'gsuite.admin.mobile.certificate.name', + type: 'keyword', + }, + 'gsuite.admin.mobile.company_owned_devices': { + category: 'gsuite', + description: 'The number of devices a company owns.', + name: 'gsuite.admin.mobile.company_owned_devices', + type: 'long', + }, + 'gsuite.admin.distribution.entity.name': { + category: 'gsuite', + description: + 'The distribution entity value, which can be a group name or an org-unit name. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-mobile-settings ', + name: 'gsuite.admin.distribution.entity.name', + type: 'keyword', + }, + 'gsuite.admin.distribution.entity.type': { + category: 'gsuite', + description: + 'The distribution entity type, which can be a group or an org-unit. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/admin-mobile-settings ', + name: 'gsuite.admin.distribution.entity.type', + type: 'keyword', + }, + 'gsuite.drive.billable': { + category: 'gsuite', + description: 'Whether this activity is billable.', + name: 'gsuite.drive.billable', + type: 'boolean', + }, + 'gsuite.drive.source_folder_id': { + category: 'gsuite', + name: 'gsuite.drive.source_folder_id', + type: 'keyword', + }, + 'gsuite.drive.source_folder_title': { + category: 'gsuite', + name: 'gsuite.drive.source_folder_title', + type: 'keyword', + }, + 'gsuite.drive.destination_folder_id': { + category: 'gsuite', + name: 'gsuite.drive.destination_folder_id', + type: 'keyword', + }, + 'gsuite.drive.destination_folder_title': { + category: 'gsuite', + name: 'gsuite.drive.destination_folder_title', + type: 'keyword', + }, + 'gsuite.drive.file.id': { + category: 'gsuite', + name: 'gsuite.drive.file.id', + type: 'keyword', + }, + 'gsuite.drive.file.type': { + category: 'gsuite', + description: + 'Document Drive type. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/drive ', + name: 'gsuite.drive.file.type', + type: 'keyword', + }, + 'gsuite.drive.originating_app_id': { + category: 'gsuite', + description: 'The Google Cloud Project ID of the application that performed the action. ', + name: 'gsuite.drive.originating_app_id', + type: 'keyword', + }, + 'gsuite.drive.file.owner.email': { + category: 'gsuite', + name: 'gsuite.drive.file.owner.email', + type: 'keyword', + }, + 'gsuite.drive.file.owner.is_shared_drive': { + category: 'gsuite', + description: 'Boolean flag denoting whether owner is a shared drive. ', + name: 'gsuite.drive.file.owner.is_shared_drive', + type: 'boolean', + }, + 'gsuite.drive.primary_event': { + category: 'gsuite', + description: + 'Whether this is a primary event. A single user action in Drive may generate several events. ', + name: 'gsuite.drive.primary_event', + type: 'boolean', + }, + 'gsuite.drive.shared_drive_id': { + category: 'gsuite', + description: + 'The unique identifier of the Team Drive. Only populated for for events relating to a Team Drive or item contained inside a Team Drive. ', + name: 'gsuite.drive.shared_drive_id', + type: 'keyword', + }, + 'gsuite.drive.visibility': { + category: 'gsuite', + description: + 'Visibility of target file. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/drive ', + name: 'gsuite.drive.visibility', + type: 'keyword', + }, + 'gsuite.drive.new_value': { + category: 'gsuite', + description: + 'When a setting or property of the file changes, the new value for it will appear here. ', + name: 'gsuite.drive.new_value', + type: 'keyword', + }, + 'gsuite.drive.old_value': { + category: 'gsuite', + description: + 'When a setting or property of the file changes, the old value for it will appear here. ', + name: 'gsuite.drive.old_value', + type: 'keyword', + }, + 'gsuite.drive.sheets_import_range_recipient_doc': { + category: 'gsuite', + description: 'Doc ID of the recipient of a sheets import range.', + name: 'gsuite.drive.sheets_import_range_recipient_doc', + type: 'keyword', + }, + 'gsuite.drive.old_visibility': { + category: 'gsuite', + description: 'When visibility changes, this holds the old value. ', + name: 'gsuite.drive.old_visibility', + type: 'keyword', + }, + 'gsuite.drive.visibility_change': { + category: 'gsuite', + description: 'When visibility changes, this holds the new overall visibility of the file. ', + name: 'gsuite.drive.visibility_change', + type: 'keyword', + }, + 'gsuite.drive.target_domain': { + category: 'gsuite', + description: + 'The domain for which the acccess scope was changed. This can also be the alias all to indicate the access scope was changed for all domains that have visibility for this document. ', + name: 'gsuite.drive.target_domain', + type: 'keyword', + }, + 'gsuite.drive.added_role': { + category: 'gsuite', + description: + 'Added membership role of a user/group in a Team Drive. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/drive ', + name: 'gsuite.drive.added_role', + type: 'keyword', + }, + 'gsuite.drive.membership_change_type': { + category: 'gsuite', + description: + 'Type of change in Team Drive membership of a user/group. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/drive ', + name: 'gsuite.drive.membership_change_type', + type: 'keyword', + }, + 'gsuite.drive.shared_drive_settings_change_type': { + category: 'gsuite', + description: + 'Type of change in Team Drive settings. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/drive ', + name: 'gsuite.drive.shared_drive_settings_change_type', + type: 'keyword', + }, + 'gsuite.drive.removed_role': { + category: 'gsuite', + description: + 'Removed membership role of a user/group in a Team Drive. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/drive ', + name: 'gsuite.drive.removed_role', + type: 'keyword', + }, + 'gsuite.drive.target': { + category: 'gsuite', + description: 'Target user or group.', + name: 'gsuite.drive.target', + type: 'keyword', + }, + 'gsuite.groups.acl_permission': { + category: 'gsuite', + description: + 'Group permission setting updated. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/groups ', + name: 'gsuite.groups.acl_permission', + type: 'keyword', + }, + 'gsuite.groups.email': { + category: 'gsuite', + description: 'Group email. ', + name: 'gsuite.groups.email', + type: 'keyword', + }, + 'gsuite.groups.member.email': { + category: 'gsuite', + description: 'Member email. ', + name: 'gsuite.groups.member.email', + type: 'keyword', + }, + 'gsuite.groups.member.role': { + category: 'gsuite', + description: + 'Member role. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/groups ', + name: 'gsuite.groups.member.role', + type: 'keyword', + }, + 'gsuite.groups.setting': { + category: 'gsuite', + description: + 'Group setting updated. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/groups ', + name: 'gsuite.groups.setting', + type: 'keyword', + }, + 'gsuite.groups.new_value': { + category: 'gsuite', + description: + 'New value(s) of the group setting. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/groups ', + name: 'gsuite.groups.new_value', + type: 'keyword', + }, + 'gsuite.groups.old_value': { + category: 'gsuite', + description: + 'Old value(s) of the group setting. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/groups', + name: 'gsuite.groups.old_value', + type: 'keyword', + }, + 'gsuite.groups.value': { + category: 'gsuite', + description: + 'Value of the group setting. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/groups ', + name: 'gsuite.groups.value', + type: 'keyword', + }, + 'gsuite.groups.message.id': { + category: 'gsuite', + description: 'SMTP message Id of an email message. Present for moderation events. ', + name: 'gsuite.groups.message.id', + type: 'keyword', + }, + 'gsuite.groups.message.moderation_action': { + category: 'gsuite', + description: 'Message moderation action. Possible values are `approved` and `rejected`. ', + name: 'gsuite.groups.message.moderation_action', + type: 'keyword', + }, + 'gsuite.groups.status': { + category: 'gsuite', + description: + 'A status describing the output of an operation. Possible values are `failed` and `succeeded`. ', + name: 'gsuite.groups.status', + type: 'keyword', + }, + 'gsuite.login.affected_email_address': { + category: 'gsuite', + name: 'gsuite.login.affected_email_address', + type: 'keyword', + }, + 'gsuite.login.challenge_method': { + category: 'gsuite', + description: + 'Login challenge method. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/login. ', + name: 'gsuite.login.challenge_method', + type: 'keyword', + }, + 'gsuite.login.failure_type': { + category: 'gsuite', + description: + 'Login failure type. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/login. ', + name: 'gsuite.login.failure_type', + type: 'keyword', + }, + 'gsuite.login.type': { + category: 'gsuite', + description: + 'Login credentials type. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/login. ', + name: 'gsuite.login.type', + type: 'keyword', + }, + 'gsuite.login.is_second_factor': { + category: 'gsuite', + name: 'gsuite.login.is_second_factor', + type: 'boolean', + }, + 'gsuite.login.is_suspicious': { + category: 'gsuite', + name: 'gsuite.login.is_suspicious', + type: 'boolean', + }, + 'gsuite.saml.application_name': { + category: 'gsuite', + description: 'Saml SP application name. ', + name: 'gsuite.saml.application_name', + type: 'keyword', + }, + 'gsuite.saml.failure_type': { + category: 'gsuite', + description: + 'Login failure type. For a list of possible values refer to https://developers.google.com/admin-sdk/reports/v1/appendix/activity/saml. ', + name: 'gsuite.saml.failure_type', + type: 'keyword', + }, + 'gsuite.saml.initiated_by': { + category: 'gsuite', + description: 'Requester of SAML authentication. ', + name: 'gsuite.saml.initiated_by', + type: 'keyword', + }, + 'gsuite.saml.orgunit_path': { + category: 'gsuite', + description: 'User orgunit. ', + name: 'gsuite.saml.orgunit_path', + type: 'keyword', + }, + 'gsuite.saml.status_code': { + category: 'gsuite', + description: 'SAML status code. ', + name: 'gsuite.saml.status_code', + type: 'long', + }, + 'gsuite.saml.second_level_status_code': { + category: 'gsuite', + description: 'SAML second level status code. ', + name: 'gsuite.saml.second_level_status_code', + type: 'long', + }, + 'ibmmq.errorlog.installation': { + category: 'ibmmq', + description: + 'This is the installation name which can be given at installation time. Each installation of IBM MQ on UNIX, Linux, and Windows, has a unique identifier known as an installation name. The installation name is used to associate things such as queue managers and configuration files with an installation. ', + name: 'ibmmq.errorlog.installation', + type: 'keyword', + }, + 'ibmmq.errorlog.qmgr': { + category: 'ibmmq', + description: + 'Name of the queue manager. Queue managers provide queuing services to applications, and manages the queues that belong to them. ', + name: 'ibmmq.errorlog.qmgr', + type: 'keyword', + }, + 'ibmmq.errorlog.arithinsert': { + category: 'ibmmq', + description: 'Changing content based on error.id', + name: 'ibmmq.errorlog.arithinsert', + type: 'keyword', + }, + 'ibmmq.errorlog.commentinsert': { + category: 'ibmmq', + description: 'Changing content based on error.id', + name: 'ibmmq.errorlog.commentinsert', + type: 'keyword', + }, + 'ibmmq.errorlog.errordescription': { + category: 'ibmmq', + description: 'Please add description', + example: 'Please add example', + name: 'ibmmq.errorlog.errordescription', + type: 'text', + }, + 'ibmmq.errorlog.explanation': { + category: 'ibmmq', + description: 'Explaines the error in more detail', + name: 'ibmmq.errorlog.explanation', + type: 'keyword', + }, + 'ibmmq.errorlog.action': { + category: 'ibmmq', + description: 'Defines what to do when the error occurs', + name: 'ibmmq.errorlog.action', + type: 'keyword', + }, + 'ibmmq.errorlog.code': { + category: 'ibmmq', + description: 'Error code.', + name: 'ibmmq.errorlog.code', + type: 'keyword', + }, + 'iptables.ether_type': { + category: 'iptables', + description: 'Value of the ethernet type field identifying the network layer protocol. ', + name: 'iptables.ether_type', + type: 'long', + }, + 'iptables.flow_label': { + category: 'iptables', + description: 'IPv6 flow label. ', + name: 'iptables.flow_label', + type: 'integer', + }, + 'iptables.fragment_flags': { + category: 'iptables', + description: 'IP fragment flags. A combination of CE, DF and MF. ', + name: 'iptables.fragment_flags', + type: 'keyword', + }, + 'iptables.fragment_offset': { + category: 'iptables', + description: 'Offset of the current IP fragment. ', + name: 'iptables.fragment_offset', + type: 'long', + }, + 'iptables.icmp.code': { + category: 'iptables', + description: 'ICMP code. ', + name: 'iptables.icmp.code', + type: 'long', + }, + 'iptables.icmp.id': { + category: 'iptables', + description: 'ICMP ID. ', + name: 'iptables.icmp.id', + type: 'long', + }, + 'iptables.icmp.parameter': { + category: 'iptables', + description: 'ICMP parameter. ', + name: 'iptables.icmp.parameter', + type: 'long', + }, + 'iptables.icmp.redirect': { + category: 'iptables', + description: 'ICMP redirect address. ', + name: 'iptables.icmp.redirect', + type: 'ip', + }, + 'iptables.icmp.seq': { + category: 'iptables', + description: 'ICMP sequence number. ', + name: 'iptables.icmp.seq', + type: 'long', + }, + 'iptables.icmp.type': { + category: 'iptables', + description: 'ICMP type. ', + name: 'iptables.icmp.type', + type: 'long', + }, + 'iptables.id': { + category: 'iptables', + description: 'Packet identifier. ', + name: 'iptables.id', + type: 'long', + }, + 'iptables.incomplete_bytes': { + category: 'iptables', + description: 'Number of incomplete bytes. ', + name: 'iptables.incomplete_bytes', + type: 'long', + }, + 'iptables.input_device': { + category: 'iptables', + description: 'Device that received the packet. ', + name: 'iptables.input_device', + type: 'keyword', + }, + 'iptables.precedence_bits': { + category: 'iptables', + description: 'IP precedence bits. ', + name: 'iptables.precedence_bits', + type: 'short', + }, + 'iptables.tos': { + category: 'iptables', + description: 'IP Type of Service field. ', + name: 'iptables.tos', + type: 'long', + }, + 'iptables.length': { + category: 'iptables', + description: 'Packet length. ', + name: 'iptables.length', + type: 'long', + }, + 'iptables.output_device': { + category: 'iptables', + description: 'Device that output the packet. ', + name: 'iptables.output_device', + type: 'keyword', + }, + 'iptables.tcp.flags': { + category: 'iptables', + description: 'TCP flags. ', + name: 'iptables.tcp.flags', + type: 'keyword', + }, + 'iptables.tcp.reserved_bits': { + category: 'iptables', + description: 'TCP reserved bits. ', + name: 'iptables.tcp.reserved_bits', + type: 'short', + }, + 'iptables.tcp.seq': { + category: 'iptables', + description: 'TCP sequence number. ', + name: 'iptables.tcp.seq', + type: 'long', + }, + 'iptables.tcp.ack': { + category: 'iptables', + description: 'TCP Acknowledgment number. ', + name: 'iptables.tcp.ack', + type: 'long', + }, + 'iptables.tcp.window': { + category: 'iptables', + description: 'Advertised TCP window size. ', + name: 'iptables.tcp.window', + type: 'long', + }, + 'iptables.ttl': { + category: 'iptables', + description: 'Time To Live field. ', + name: 'iptables.ttl', + type: 'integer', + }, + 'iptables.udp.length': { + category: 'iptables', + description: 'Length of the UDP header and payload. ', + name: 'iptables.udp.length', + type: 'long', + }, + 'iptables.ubiquiti.input_zone': { + category: 'iptables', + description: 'Input zone. ', + name: 'iptables.ubiquiti.input_zone', + type: 'keyword', + }, + 'iptables.ubiquiti.output_zone': { + category: 'iptables', + description: 'Output zone. ', + name: 'iptables.ubiquiti.output_zone', + type: 'keyword', + }, + 'iptables.ubiquiti.rule_number': { + category: 'iptables', + description: 'The rule number within the rule set.', + name: 'iptables.ubiquiti.rule_number', + type: 'keyword', + }, + 'iptables.ubiquiti.rule_set': { + category: 'iptables', + description: 'The rule set name.', + name: 'iptables.ubiquiti.rule_set', + type: 'keyword', + }, + 'microsoft.defender_atp.lastUpdateTime': { + category: 'microsoft', + description: 'The date and time (in UTC) the alert was last updated. ', + name: 'microsoft.defender_atp.lastUpdateTime', + type: 'date', + }, + 'microsoft.defender_atp.resolvedTime': { + category: 'microsoft', + description: "The date and time in which the status of the alert was changed to 'Resolved'. ", + name: 'microsoft.defender_atp.resolvedTime', + type: 'date', + }, + 'microsoft.defender_atp.incidentId': { + category: 'microsoft', + description: 'The Incident ID of the Alert. ', + name: 'microsoft.defender_atp.incidentId', + type: 'keyword', + }, + 'microsoft.defender_atp.investigationId': { + category: 'microsoft', + description: 'The Investigation ID related to the Alert. ', + name: 'microsoft.defender_atp.investigationId', + type: 'keyword', + }, + 'microsoft.defender_atp.investigationState': { + category: 'microsoft', + description: 'The current state of the Investigation. ', + name: 'microsoft.defender_atp.investigationState', + type: 'keyword', + }, + 'microsoft.defender_atp.assignedTo': { + category: 'microsoft', + description: 'Owner of the alert. ', + name: 'microsoft.defender_atp.assignedTo', + type: 'keyword', + }, + 'microsoft.defender_atp.status': { + category: 'microsoft', + description: + "Specifies the current status of the alert. Possible values are: 'Unknown', 'New', 'InProgress' and 'Resolved'. ", + name: 'microsoft.defender_atp.status', + type: 'keyword', + }, + 'microsoft.defender_atp.classification': { + category: 'microsoft', + description: + "Specification of the alert. Possible values are: 'Unknown', 'FalsePositive', 'TruePositive'. ", + name: 'microsoft.defender_atp.classification', + type: 'keyword', + }, + 'microsoft.defender_atp.determination': { + category: 'microsoft', + description: + "Specifies the determination of the alert. Possible values are: 'NotAvailable', 'Apt', 'Malware', 'SecurityPersonnel', 'SecurityTesting', 'UnwantedSoftware', 'Other'. ", + name: 'microsoft.defender_atp.determination', + type: 'keyword', + }, + 'microsoft.defender_atp.threatFamilyName': { + category: 'microsoft', + description: 'Threat family. ', + name: 'microsoft.defender_atp.threatFamilyName', + type: 'keyword', + }, + 'microsoft.defender_atp.rbacGroupName': { + category: 'microsoft', + description: 'User group related to the alert ', + name: 'microsoft.defender_atp.rbacGroupName', + type: 'keyword', + }, + 'microsoft.defender_atp.evidence.domainName': { + category: 'microsoft', + description: 'Domain name related to the alert ', + name: 'microsoft.defender_atp.evidence.domainName', + type: 'keyword', + }, + 'microsoft.defender_atp.evidence.ipAddress': { + category: 'microsoft', + description: 'IP address involved in the alert ', + name: 'microsoft.defender_atp.evidence.ipAddress', + type: 'ip', + }, + 'microsoft.defender_atp.evidence.aadUserId': { + category: 'microsoft', + description: 'ID of the user involved in the alert ', + name: 'microsoft.defender_atp.evidence.aadUserId', + type: 'keyword', + }, + 'microsoft.defender_atp.evidence.accountName': { + category: 'microsoft', + description: 'Username of the user involved in the alert ', + name: 'microsoft.defender_atp.evidence.accountName', + type: 'keyword', + }, + 'microsoft.defender_atp.evidence.entityType': { + category: 'microsoft', + description: 'The type of evidence ', + name: 'microsoft.defender_atp.evidence.entityType', + type: 'keyword', + }, + 'microsoft.defender_atp.evidence.userPrincipalName': { + category: 'microsoft', + description: 'Principal name of the user involved in the alert ', + name: 'microsoft.defender_atp.evidence.userPrincipalName', + type: 'keyword', + }, + 'misp.attack_pattern.id': { + category: 'misp', + description: 'Identifier of the threat indicator. ', + name: 'misp.attack_pattern.id', + type: 'keyword', + }, + 'misp.attack_pattern.name': { + category: 'misp', + description: 'Name of the attack pattern. ', + name: 'misp.attack_pattern.name', + type: 'keyword', + }, + 'misp.attack_pattern.description': { + category: 'misp', + description: 'Description of the attack pattern. ', + name: 'misp.attack_pattern.description', + type: 'text', + }, + 'misp.attack_pattern.kill_chain_phases': { + category: 'misp', + description: 'The kill chain phase(s) to which this attack pattern corresponds. ', + name: 'misp.attack_pattern.kill_chain_phases', + type: 'keyword', + }, + 'misp.campaign.id': { + category: 'misp', + description: 'Identifier of the campaign. ', + name: 'misp.campaign.id', + type: 'keyword', + }, + 'misp.campaign.name': { + category: 'misp', + description: 'Name of the campaign. ', + name: 'misp.campaign.name', + type: 'keyword', + }, + 'misp.campaign.description': { + category: 'misp', + description: 'Description of the campaign. ', + name: 'misp.campaign.description', + type: 'text', + }, + 'misp.campaign.aliases': { + category: 'misp', + description: 'Alternative names used to identify this campaign. ', + name: 'misp.campaign.aliases', + type: 'text', + }, + 'misp.campaign.first_seen': { + category: 'misp', + description: 'The time that this Campaign was first seen, in RFC3339 format. ', + name: 'misp.campaign.first_seen', + type: 'date', + }, + 'misp.campaign.last_seen': { + category: 'misp', + description: 'The time that this Campaign was last seen, in RFC3339 format. ', + name: 'misp.campaign.last_seen', + type: 'date', + }, + 'misp.campaign.objective': { + category: 'misp', + description: + "This field defines the Campaign's primary goal, objective, desired outcome, or intended effect. ", + name: 'misp.campaign.objective', + type: 'keyword', + }, + 'misp.course_of_action.id': { + category: 'misp', + description: 'Identifier of the Course of Action. ', + name: 'misp.course_of_action.id', + type: 'keyword', + }, + 'misp.course_of_action.name': { + category: 'misp', + description: 'The name used to identify the Course of Action. ', + name: 'misp.course_of_action.name', + type: 'keyword', + }, + 'misp.course_of_action.description': { + category: 'misp', + description: 'Description of the Course of Action. ', + name: 'misp.course_of_action.description', + type: 'text', + }, + 'misp.identity.id': { + category: 'misp', + description: 'Identifier of the Identity. ', + name: 'misp.identity.id', + type: 'keyword', + }, + 'misp.identity.name': { + category: 'misp', + description: 'The name used to identify the Identity. ', + name: 'misp.identity.name', + type: 'keyword', + }, + 'misp.identity.description': { + category: 'misp', + description: 'Description of the Identity. ', + name: 'misp.identity.description', + type: 'text', + }, + 'misp.identity.identity_class': { + category: 'misp', + description: + 'The type of entity that this Identity describes, e.g., an individual or organization. Open Vocab - identity-class-ov ', + name: 'misp.identity.identity_class', + type: 'keyword', + }, + 'misp.identity.labels': { + category: 'misp', + description: 'The list of roles that this Identity performs. ', + example: 'CEO\n', + name: 'misp.identity.labels', + type: 'keyword', + }, + 'misp.identity.sectors': { + category: 'misp', + description: + 'The list of sectors that this Identity belongs to. Open Vocab - industry-sector-ov ', + name: 'misp.identity.sectors', + type: 'keyword', + }, + 'misp.identity.contact_information': { + category: 'misp', + description: 'The contact information (e-mail, phone number, etc.) for this Identity. ', + name: 'misp.identity.contact_information', + type: 'text', + }, + 'misp.intrusion_set.id': { + category: 'misp', + description: 'Identifier of the Intrusion Set. ', + name: 'misp.intrusion_set.id', + type: 'keyword', + }, + 'misp.intrusion_set.name': { + category: 'misp', + description: 'The name used to identify the Intrusion Set. ', + name: 'misp.intrusion_set.name', + type: 'keyword', + }, + 'misp.intrusion_set.description': { + category: 'misp', + description: 'Description of the Intrusion Set. ', + name: 'misp.intrusion_set.description', + type: 'text', + }, + 'misp.intrusion_set.aliases': { + category: 'misp', + description: 'Alternative names used to identify the Intrusion Set. ', + name: 'misp.intrusion_set.aliases', + type: 'text', + }, + 'misp.intrusion_set.first_seen': { + category: 'misp', + description: 'The time that this Intrusion Set was first seen, in RFC3339 format. ', + name: 'misp.intrusion_set.first_seen', + type: 'date', + }, + 'misp.intrusion_set.last_seen': { + category: 'misp', + description: 'The time that this Intrusion Set was last seen, in RFC3339 format. ', + name: 'misp.intrusion_set.last_seen', + type: 'date', + }, + 'misp.intrusion_set.goals': { + category: 'misp', + description: 'The high level goals of this Intrusion Set, namely, what are they trying to do. ', + name: 'misp.intrusion_set.goals', + type: 'text', + }, + 'misp.intrusion_set.resource_level': { + category: 'misp', + description: + 'This defines the organizational level at which this Intrusion Set typically works. Open Vocab - attack-resource-level-ov ', + name: 'misp.intrusion_set.resource_level', + type: 'text', + }, + 'misp.intrusion_set.primary_motivation': { + category: 'misp', + description: + 'The primary reason, motivation, or purpose behind this Intrusion Set. Open Vocab - attack-motivation-ov ', + name: 'misp.intrusion_set.primary_motivation', + type: 'text', + }, + 'misp.intrusion_set.secondary_motivations': { + category: 'misp', + description: + 'The secondary reasons, motivations, or purposes behind this Intrusion Set. Open Vocab - attack-motivation-ov ', + name: 'misp.intrusion_set.secondary_motivations', + type: 'text', + }, + 'misp.malware.id': { + category: 'misp', + description: 'Identifier of the Malware. ', + name: 'misp.malware.id', + type: 'keyword', + }, + 'misp.malware.name': { + category: 'misp', + description: 'The name used to identify the Malware. ', + name: 'misp.malware.name', + type: 'keyword', + }, + 'misp.malware.description': { + category: 'misp', + description: 'Description of the Malware. ', + name: 'misp.malware.description', + type: 'text', + }, + 'misp.malware.labels': { + category: 'misp', + description: + 'The type of malware being described. Open Vocab - malware-label-ov. adware,backdoor,bot,ddos,dropper,exploit-kit,keylogger,ransomware, remote-access-trojan,resource-exploitation,rogue-security-software,rootkit, screen-capture,spyware,trojan,virus,worm ', + name: 'misp.malware.labels', + type: 'keyword', + }, + 'misp.malware.kill_chain_phases': { + category: 'misp', + description: 'The list of kill chain phases for which this Malware instance can be used. ', + name: 'misp.malware.kill_chain_phases', + type: 'keyword', + format: 'string', + }, + 'misp.note.id': { + category: 'misp', + description: 'Identifier of the Note. ', + name: 'misp.note.id', + type: 'keyword', + }, + 'misp.note.summary': { + category: 'misp', + description: 'A brief description used as a summary of the Note. ', + name: 'misp.note.summary', + type: 'keyword', + }, + 'misp.note.description': { + category: 'misp', + description: 'The content of the Note. ', + name: 'misp.note.description', + type: 'text', + }, + 'misp.note.authors': { + category: 'misp', + description: 'The name of the author(s) of this Note. ', + name: 'misp.note.authors', + type: 'keyword', + }, + 'misp.note.object_refs': { + category: 'misp', + description: 'The STIX Objects (SDOs and SROs) that the note is being applied to. ', + name: 'misp.note.object_refs', + type: 'keyword', + }, + 'misp.threat_indicator.labels': { + category: 'misp', + description: 'list of type open-vocab that specifies the type of indicator. ', + example: 'Domain Watchlist\n', + name: 'misp.threat_indicator.labels', + type: 'keyword', + }, + 'misp.threat_indicator.id': { + category: 'misp', + description: 'Identifier of the threat indicator. ', + name: 'misp.threat_indicator.id', + type: 'keyword', + }, + 'misp.threat_indicator.version': { + category: 'misp', + description: 'Version of the threat indicator. ', + name: 'misp.threat_indicator.version', + type: 'keyword', + }, + 'misp.threat_indicator.type': { + category: 'misp', + description: 'Type of the threat indicator. ', + name: 'misp.threat_indicator.type', + type: 'keyword', + }, + 'misp.threat_indicator.description': { + category: 'misp', + description: 'Description of the threat indicator. ', + name: 'misp.threat_indicator.description', + type: 'text', + }, + 'misp.threat_indicator.feed': { + category: 'misp', + description: 'Name of the threat feed. ', + name: 'misp.threat_indicator.feed', + type: 'text', + }, + 'misp.threat_indicator.valid_from': { + category: 'misp', + description: + 'The time from which this Indicator should be considered valuable intelligence, in RFC3339 format. ', + name: 'misp.threat_indicator.valid_from', + type: 'date', + }, + 'misp.threat_indicator.valid_until': { + category: 'misp', + description: + 'The time at which this Indicator should no longer be considered valuable intelligence. If the valid_until property is omitted, then there is no constraint on the latest time for which the indicator should be used, in RFC3339 format. ', + name: 'misp.threat_indicator.valid_until', + type: 'date', + }, + 'misp.threat_indicator.severity': { + category: 'misp', + description: 'Threat severity to which this indicator corresponds. ', + example: 'high', + name: 'misp.threat_indicator.severity', + type: 'keyword', + format: 'string', + }, + 'misp.threat_indicator.confidence': { + category: 'misp', + description: 'Confidence level to which this indicator corresponds. ', + example: 'high', + name: 'misp.threat_indicator.confidence', + type: 'keyword', + }, + 'misp.threat_indicator.kill_chain_phases': { + category: 'misp', + description: 'The kill chain phase(s) to which this indicator corresponds. ', + name: 'misp.threat_indicator.kill_chain_phases', + type: 'keyword', + format: 'string', + }, + 'misp.threat_indicator.mitre_tactic': { + category: 'misp', + description: 'MITRE tactics to which this indicator corresponds. ', + example: 'Initial Access', + name: 'misp.threat_indicator.mitre_tactic', + type: 'keyword', + format: 'string', + }, + 'misp.threat_indicator.mitre_technique': { + category: 'misp', + description: 'MITRE techniques to which this indicator corresponds. ', + example: 'Drive-by Compromise', + name: 'misp.threat_indicator.mitre_technique', + type: 'keyword', + format: 'string', + }, + 'misp.threat_indicator.attack_pattern': { + category: 'misp', + description: + 'The attack_pattern for this indicator is a STIX Pattern as specified in STIX Version 2.0 Part 5 - STIX Patterning. ', + example: "[destination:ip = '91.219.29.188/32']\n", + name: 'misp.threat_indicator.attack_pattern', + type: 'keyword', + }, + 'misp.threat_indicator.attack_pattern_kql': { + category: 'misp', + description: + 'The attack_pattern for this indicator is KQL query that matches the attack_pattern specified in the STIX Pattern format. ', + example: 'destination.ip: "91.219.29.188/32"\n', + name: 'misp.threat_indicator.attack_pattern_kql', + type: 'keyword', + }, + 'misp.threat_indicator.negate': { + category: 'misp', + description: 'When set to true, it specifies the absence of the attack_pattern. ', + name: 'misp.threat_indicator.negate', + type: 'boolean', + }, + 'misp.threat_indicator.intrusion_set': { + category: 'misp', + description: 'Name of the intrusion set if known. ', + name: 'misp.threat_indicator.intrusion_set', + type: 'keyword', + }, + 'misp.threat_indicator.campaign': { + category: 'misp', + description: 'Name of the attack campaign if known. ', + name: 'misp.threat_indicator.campaign', + type: 'keyword', + }, + 'misp.threat_indicator.threat_actor': { + category: 'misp', + description: 'Name of the threat actor if known. ', + name: 'misp.threat_indicator.threat_actor', + type: 'keyword', + }, + 'misp.observed_data.id': { + category: 'misp', + description: 'Identifier of the Observed Data. ', + name: 'misp.observed_data.id', + type: 'keyword', + }, + 'misp.observed_data.first_observed': { + category: 'misp', + description: 'The beginning of the time window that the data was observed, in RFC3339 format. ', + name: 'misp.observed_data.first_observed', + type: 'date', + }, + 'misp.observed_data.last_observed': { + category: 'misp', + description: 'The end of the time window that the data was observed, in RFC3339 format. ', + name: 'misp.observed_data.last_observed', + type: 'date', + }, + 'misp.observed_data.number_observed': { + category: 'misp', + description: + 'The number of times the data represented in the objects property was observed. This MUST be an integer between 1 and 999,999,999 inclusive. ', + name: 'misp.observed_data.number_observed', + type: 'integer', + }, + 'misp.observed_data.objects': { + category: 'misp', + description: + 'A dictionary of Cyber Observable Objects that describes the single fact that was observed. ', + name: 'misp.observed_data.objects', + type: 'keyword', + }, + 'misp.report.id': { + category: 'misp', + description: 'Identifier of the Report. ', + name: 'misp.report.id', + type: 'keyword', + }, + 'misp.report.labels': { + category: 'misp', + description: + 'This field is an Open Vocabulary that specifies the primary subject of this report. Open Vocab - report-label-ov. threat-report,attack-pattern,campaign,identity,indicator,malware,observed-data,threat-actor,tool,vulnerability ', + name: 'misp.report.labels', + type: 'keyword', + }, + 'misp.report.name': { + category: 'misp', + description: 'The name used to identify the Report. ', + name: 'misp.report.name', + type: 'keyword', + }, + 'misp.report.description': { + category: 'misp', + description: 'A description that provides more details and context about Report. ', + name: 'misp.report.description', + type: 'text', + }, + 'misp.report.published': { + category: 'misp', + description: + 'The date that this report object was officially published by the creator of this report, in RFC3339 format. ', + name: 'misp.report.published', + type: 'date', + }, + 'misp.report.object_refs': { + category: 'misp', + description: 'Specifies the STIX Objects that are referred to by this Report. ', + name: 'misp.report.object_refs', + type: 'text', + }, + 'misp.threat_actor.id': { + category: 'misp', + description: 'Identifier of the Threat Actor. ', + name: 'misp.threat_actor.id', + type: 'keyword', + }, + 'misp.threat_actor.labels': { + category: 'misp', + description: + 'This field specifies the type of threat actor. Open Vocab - threat-actor-label-ov. activist,competitor,crime-syndicate,criminal,hacker,insider-accidental,insider-disgruntled,nation-state,sensationalist,spy,terrorist ', + name: 'misp.threat_actor.labels', + type: 'keyword', + }, + 'misp.threat_actor.name': { + category: 'misp', + description: 'The name used to identify this Threat Actor or Threat Actor group. ', + name: 'misp.threat_actor.name', + type: 'keyword', + }, + 'misp.threat_actor.description': { + category: 'misp', + description: 'A description that provides more details and context about the Threat Actor. ', + name: 'misp.threat_actor.description', + type: 'text', + }, + 'misp.threat_actor.aliases': { + category: 'misp', + description: 'A list of other names that this Threat Actor is believed to use. ', + name: 'misp.threat_actor.aliases', + type: 'text', + }, + 'misp.threat_actor.roles': { + category: 'misp', + description: + 'This is a list of roles the Threat Actor plays. Open Vocab - threat-actor-role-ov. agent,director,independent,sponsor,infrastructure-operator,infrastructure-architect,malware-author ', + name: 'misp.threat_actor.roles', + type: 'text', + }, + 'misp.threat_actor.goals': { + category: 'misp', + description: 'The high level goals of this Threat Actor, namely, what are they trying to do. ', + name: 'misp.threat_actor.goals', + type: 'text', + }, + 'misp.threat_actor.sophistication': { + category: 'misp', + description: + 'The skill, specific knowledge, special training, or expertise a Threat Actor must have to perform the attack. Open Vocab - threat-actor-sophistication-ov. none,minimal,intermediate,advanced,strategic,expert,innovator ', + name: 'misp.threat_actor.sophistication', + type: 'text', + }, + 'misp.threat_actor.resource_level': { + category: 'misp', + description: + 'This defines the organizational level at which this Threat Actor typically works. Open Vocab - attack-resource-level-ov. individual,club,contest,team,organization,government ', + name: 'misp.threat_actor.resource_level', + type: 'text', + }, + 'misp.threat_actor.primary_motivation': { + category: 'misp', + description: + 'The primary reason, motivation, or purpose behind this Threat Actor. Open Vocab - attack-motivation-ov. accidental,coercion,dominance,ideology,notoriety,organizational-gain,personal-gain,personal-satisfaction,revenge,unpredictable ', + name: 'misp.threat_actor.primary_motivation', + type: 'text', + }, + 'misp.threat_actor.secondary_motivations': { + category: 'misp', + description: + 'The secondary reasons, motivations, or purposes behind this Threat Actor. Open Vocab - attack-motivation-ov. accidental,coercion,dominance,ideology,notoriety,organizational-gain,personal-gain,personal-satisfaction,revenge,unpredictable ', + name: 'misp.threat_actor.secondary_motivations', + type: 'text', + }, + 'misp.threat_actor.personal_motivations': { + category: 'misp', + description: + 'The personal reasons, motivations, or purposes of the Threat Actor regardless of organizational goals. Open Vocab - attack-motivation-ov. accidental,coercion,dominance,ideology,notoriety,organizational-gain,personal-gain,personal-satisfaction,revenge,unpredictable ', + name: 'misp.threat_actor.personal_motivations', + type: 'text', + }, + 'misp.tool.id': { + category: 'misp', + description: 'Identifier of the Tool. ', + name: 'misp.tool.id', + type: 'keyword', + }, + 'misp.tool.labels': { + category: 'misp', + description: + 'The kind(s) of tool(s) being described. Open Vocab - tool-label-ov. denial-of-service,exploitation,information-gathering,network-capture,credential-exploitation,remote-access,vulnerability-scanning ', + name: 'misp.tool.labels', + type: 'keyword', + }, + 'misp.tool.name': { + category: 'misp', + description: 'The name used to identify the Tool. ', + name: 'misp.tool.name', + type: 'keyword', + }, + 'misp.tool.description': { + category: 'misp', + description: 'A description that provides more details and context about the Tool. ', + name: 'misp.tool.description', + type: 'text', + }, + 'misp.tool.tool_version': { + category: 'misp', + description: 'The version identifier associated with the Tool. ', + name: 'misp.tool.tool_version', + type: 'keyword', + }, + 'misp.tool.kill_chain_phases': { + category: 'misp', + description: 'The list of kill chain phases for which this Tool instance can be used. ', + name: 'misp.tool.kill_chain_phases', + type: 'text', + }, + 'misp.vulnerability.id': { + category: 'misp', + description: 'Identifier of the Vulnerability. ', + name: 'misp.vulnerability.id', + type: 'keyword', + }, + 'misp.vulnerability.name': { + category: 'misp', + description: 'The name used to identify the Vulnerability. ', + name: 'misp.vulnerability.name', + type: 'keyword', + }, + 'misp.vulnerability.description': { + category: 'misp', + description: 'A description that provides more details and context about the Vulnerability. ', + name: 'misp.vulnerability.description', + type: 'text', + }, + 'mssql.log.origin': { + category: 'mssql', + description: 'Origin of the message, usually the server but it can also be a recovery process', + name: 'mssql.log.origin', + type: 'keyword', + }, + 'o365.audit.Actor.ID': { + category: 'o365', + name: 'o365.audit.Actor.ID', + type: 'keyword', + }, + 'o365.audit.Actor.Type': { + category: 'o365', + name: 'o365.audit.Actor.Type', + type: 'keyword', + }, + 'o365.audit.ActorContextId': { + category: 'o365', + name: 'o365.audit.ActorContextId', + type: 'keyword', + }, + 'o365.audit.ActorIpAddress': { + category: 'o365', + name: 'o365.audit.ActorIpAddress', + type: 'keyword', + }, + 'o365.audit.ActorUserId': { + category: 'o365', + name: 'o365.audit.ActorUserId', + type: 'keyword', + }, + 'o365.audit.ActorYammerUserId': { + category: 'o365', + name: 'o365.audit.ActorYammerUserId', + type: 'keyword', + }, + 'o365.audit.AlertEntityId': { + category: 'o365', + name: 'o365.audit.AlertEntityId', + type: 'keyword', + }, + 'o365.audit.AlertId': { + category: 'o365', + name: 'o365.audit.AlertId', + type: 'keyword', + }, + 'o365.audit.AlertLinks': { + category: 'o365', + name: 'o365.audit.AlertLinks', + type: 'array', + }, + 'o365.audit.AlertType': { + category: 'o365', + name: 'o365.audit.AlertType', + type: 'keyword', + }, + 'o365.audit.AppId': { + category: 'o365', + name: 'o365.audit.AppId', + type: 'keyword', + }, + 'o365.audit.ApplicationDisplayName': { + category: 'o365', + name: 'o365.audit.ApplicationDisplayName', + type: 'keyword', + }, + 'o365.audit.ApplicationId': { + category: 'o365', + name: 'o365.audit.ApplicationId', + type: 'keyword', + }, + 'o365.audit.AzureActiveDirectoryEventType': { + category: 'o365', + name: 'o365.audit.AzureActiveDirectoryEventType', + type: 'keyword', + }, + 'o365.audit.ExchangeMetaData.*': { + category: 'o365', + name: 'o365.audit.ExchangeMetaData.*', + type: 'object', + }, + 'o365.audit.Category': { + category: 'o365', + name: 'o365.audit.Category', + type: 'keyword', + }, + 'o365.audit.ClientAppId': { + category: 'o365', + name: 'o365.audit.ClientAppId', + type: 'keyword', + }, + 'o365.audit.ClientInfoString': { + category: 'o365', + name: 'o365.audit.ClientInfoString', + type: 'keyword', + }, + 'o365.audit.ClientIP': { + category: 'o365', + name: 'o365.audit.ClientIP', + type: 'keyword', + }, + 'o365.audit.ClientIPAddress': { + category: 'o365', + name: 'o365.audit.ClientIPAddress', + type: 'keyword', + }, + 'o365.audit.Comments': { + category: 'o365', + name: 'o365.audit.Comments', + type: 'text', + }, + 'o365.audit.CorrelationId': { + category: 'o365', + name: 'o365.audit.CorrelationId', + type: 'keyword', + }, + 'o365.audit.CreationTime': { + category: 'o365', + name: 'o365.audit.CreationTime', + type: 'keyword', + }, + 'o365.audit.CustomUniqueId': { + category: 'o365', + name: 'o365.audit.CustomUniqueId', + type: 'keyword', + }, + 'o365.audit.Data': { + category: 'o365', + name: 'o365.audit.Data', + type: 'keyword', + }, + 'o365.audit.DataType': { + category: 'o365', + name: 'o365.audit.DataType', + type: 'keyword', + }, + 'o365.audit.EntityType': { + category: 'o365', + name: 'o365.audit.EntityType', + type: 'keyword', + }, + 'o365.audit.EventData': { + category: 'o365', + name: 'o365.audit.EventData', + type: 'keyword', + }, + 'o365.audit.EventSource': { + category: 'o365', + name: 'o365.audit.EventSource', + type: 'keyword', + }, + 'o365.audit.ExceptionInfo.*': { + category: 'o365', + name: 'o365.audit.ExceptionInfo.*', + type: 'object', + }, + 'o365.audit.ExtendedProperties.*': { + category: 'o365', + name: 'o365.audit.ExtendedProperties.*', + type: 'object', + }, + 'o365.audit.ExternalAccess': { + category: 'o365', + name: 'o365.audit.ExternalAccess', + type: 'keyword', + }, + 'o365.audit.GroupName': { + category: 'o365', + name: 'o365.audit.GroupName', + type: 'keyword', + }, + 'o365.audit.Id': { + category: 'o365', + name: 'o365.audit.Id', + type: 'keyword', + }, + 'o365.audit.ImplicitShare': { + category: 'o365', + name: 'o365.audit.ImplicitShare', + type: 'keyword', + }, + 'o365.audit.IncidentId': { + category: 'o365', + name: 'o365.audit.IncidentId', + type: 'keyword', + }, + 'o365.audit.InternalLogonType': { + category: 'o365', + name: 'o365.audit.InternalLogonType', + type: 'keyword', + }, + 'o365.audit.InterSystemsId': { + category: 'o365', + name: 'o365.audit.InterSystemsId', + type: 'keyword', + }, + 'o365.audit.IntraSystemId': { + category: 'o365', + name: 'o365.audit.IntraSystemId', + type: 'keyword', + }, + 'o365.audit.Item.*': { + category: 'o365', + name: 'o365.audit.Item.*', + type: 'object', + }, + 'o365.audit.Item.*.*': { + category: 'o365', + name: 'o365.audit.Item.*.*', + type: 'object', + }, + 'o365.audit.ItemName': { + category: 'o365', + name: 'o365.audit.ItemName', + type: 'keyword', + }, + 'o365.audit.ItemType': { + category: 'o365', + name: 'o365.audit.ItemType', + type: 'keyword', + }, + 'o365.audit.ListId': { + category: 'o365', + name: 'o365.audit.ListId', + type: 'keyword', + }, + 'o365.audit.ListItemUniqueId': { + category: 'o365', + name: 'o365.audit.ListItemUniqueId', + type: 'keyword', + }, + 'o365.audit.LogonError': { + category: 'o365', + name: 'o365.audit.LogonError', + type: 'keyword', + }, + 'o365.audit.LogonType': { + category: 'o365', + name: 'o365.audit.LogonType', + type: 'keyword', + }, + 'o365.audit.LogonUserSid': { + category: 'o365', + name: 'o365.audit.LogonUserSid', + type: 'keyword', + }, + 'o365.audit.MailboxGuid': { + category: 'o365', + name: 'o365.audit.MailboxGuid', + type: 'keyword', + }, + 'o365.audit.MailboxOwnerMasterAccountSid': { + category: 'o365', + name: 'o365.audit.MailboxOwnerMasterAccountSid', + type: 'keyword', + }, + 'o365.audit.MailboxOwnerSid': { + category: 'o365', + name: 'o365.audit.MailboxOwnerSid', + type: 'keyword', + }, + 'o365.audit.MailboxOwnerUPN': { + category: 'o365', + name: 'o365.audit.MailboxOwnerUPN', + type: 'keyword', + }, + 'o365.audit.Members': { + category: 'o365', + name: 'o365.audit.Members', + type: 'array', + }, + 'o365.audit.Members.*': { + category: 'o365', + name: 'o365.audit.Members.*', + type: 'object', + }, + 'o365.audit.ModifiedProperties.*.*': { + category: 'o365', + name: 'o365.audit.ModifiedProperties.*.*', + type: 'object', + }, + 'o365.audit.Name': { + category: 'o365', + name: 'o365.audit.Name', + type: 'keyword', + }, + 'o365.audit.ObjectId': { + category: 'o365', + name: 'o365.audit.ObjectId', + type: 'keyword', + }, + 'o365.audit.Operation': { + category: 'o365', + name: 'o365.audit.Operation', + type: 'keyword', + }, + 'o365.audit.OrganizationId': { + category: 'o365', + name: 'o365.audit.OrganizationId', + type: 'keyword', + }, + 'o365.audit.OrganizationName': { + category: 'o365', + name: 'o365.audit.OrganizationName', + type: 'keyword', + }, + 'o365.audit.OriginatingServer': { + category: 'o365', + name: 'o365.audit.OriginatingServer', + type: 'keyword', + }, + 'o365.audit.Parameters.*': { + category: 'o365', + name: 'o365.audit.Parameters.*', + type: 'object', + }, + 'o365.audit.PolicyDetails': { + category: 'o365', + name: 'o365.audit.PolicyDetails', + type: 'array', + }, + 'o365.audit.PolicyId': { + category: 'o365', + name: 'o365.audit.PolicyId', + type: 'keyword', + }, + 'o365.audit.RecordType': { + category: 'o365', + name: 'o365.audit.RecordType', + type: 'keyword', + }, + 'o365.audit.ResultStatus': { + category: 'o365', + name: 'o365.audit.ResultStatus', + type: 'keyword', + }, + 'o365.audit.SensitiveInfoDetectionIsIncluded': { + category: 'o365', + name: 'o365.audit.SensitiveInfoDetectionIsIncluded', + type: 'keyword', + }, + 'o365.audit.SharePointMetaData.*': { + category: 'o365', + name: 'o365.audit.SharePointMetaData.*', + type: 'object', + }, + 'o365.audit.SessionId': { + category: 'o365', + name: 'o365.audit.SessionId', + type: 'keyword', + }, + 'o365.audit.Severity': { + category: 'o365', + name: 'o365.audit.Severity', + type: 'keyword', + }, + 'o365.audit.Site': { + category: 'o365', + name: 'o365.audit.Site', + type: 'keyword', + }, + 'o365.audit.SiteUrl': { + category: 'o365', + name: 'o365.audit.SiteUrl', + type: 'keyword', + }, + 'o365.audit.Source': { + category: 'o365', + name: 'o365.audit.Source', + type: 'keyword', + }, + 'o365.audit.SourceFileExtension': { + category: 'o365', + name: 'o365.audit.SourceFileExtension', + type: 'keyword', + }, + 'o365.audit.SourceFileName': { + category: 'o365', + name: 'o365.audit.SourceFileName', + type: 'keyword', + }, + 'o365.audit.SourceRelativeUrl': { + category: 'o365', + name: 'o365.audit.SourceRelativeUrl', + type: 'keyword', + }, + 'o365.audit.Status': { + category: 'o365', + name: 'o365.audit.Status', + type: 'keyword', + }, + 'o365.audit.SupportTicketId': { + category: 'o365', + name: 'o365.audit.SupportTicketId', + type: 'keyword', + }, + 'o365.audit.Target.ID': { + category: 'o365', + name: 'o365.audit.Target.ID', + type: 'keyword', + }, + 'o365.audit.Target.Type': { + category: 'o365', + name: 'o365.audit.Target.Type', + type: 'keyword', + }, + 'o365.audit.TargetContextId': { + category: 'o365', + name: 'o365.audit.TargetContextId', + type: 'keyword', + }, + 'o365.audit.TargetUserOrGroupName': { + category: 'o365', + name: 'o365.audit.TargetUserOrGroupName', + type: 'keyword', + }, + 'o365.audit.TargetUserOrGroupType': { + category: 'o365', + name: 'o365.audit.TargetUserOrGroupType', + type: 'keyword', + }, + 'o365.audit.TeamName': { + category: 'o365', + name: 'o365.audit.TeamName', + type: 'keyword', + }, + 'o365.audit.TeamGuid': { + category: 'o365', + name: 'o365.audit.TeamGuid', + type: 'keyword', + }, + 'o365.audit.UniqueSharingId': { + category: 'o365', + name: 'o365.audit.UniqueSharingId', + type: 'keyword', + }, + 'o365.audit.UserAgent': { + category: 'o365', + name: 'o365.audit.UserAgent', + type: 'keyword', + }, + 'o365.audit.UserId': { + category: 'o365', + name: 'o365.audit.UserId', + type: 'keyword', + }, + 'o365.audit.UserKey': { + category: 'o365', + name: 'o365.audit.UserKey', + type: 'keyword', + }, + 'o365.audit.UserType': { + category: 'o365', + name: 'o365.audit.UserType', + type: 'keyword', + }, + 'o365.audit.Version': { + category: 'o365', + name: 'o365.audit.Version', + type: 'keyword', + }, + 'o365.audit.WebId': { + category: 'o365', + name: 'o365.audit.WebId', + type: 'keyword', + }, + 'o365.audit.Workload': { + category: 'o365', + name: 'o365.audit.Workload', + type: 'keyword', + }, + 'o365.audit.YammerNetworkId': { + category: 'o365', + name: 'o365.audit.YammerNetworkId', + type: 'keyword', + }, + 'okta.uuid': { + category: 'okta', + description: 'The unique identifier of the Okta LogEvent. ', + name: 'okta.uuid', + type: 'keyword', + }, + 'okta.event_type': { + category: 'okta', + description: 'The type of the LogEvent. ', + name: 'okta.event_type', + type: 'keyword', + }, + 'okta.version': { + category: 'okta', + description: 'The version of the LogEvent. ', + name: 'okta.version', + type: 'keyword', + }, + 'okta.severity': { + category: 'okta', + description: 'The severity of the LogEvent. Must be one of DEBUG, INFO, WARN, or ERROR. ', + name: 'okta.severity', + type: 'keyword', + }, + 'okta.display_message': { + category: 'okta', + description: 'The display message of the LogEvent. ', + name: 'okta.display_message', + type: 'keyword', + }, + 'okta.actor.id': { + category: 'okta', + description: 'Identifier of the actor. ', + name: 'okta.actor.id', + type: 'keyword', + }, + 'okta.actor.type': { + category: 'okta', + description: 'Type of the actor. ', + name: 'okta.actor.type', + type: 'keyword', + }, + 'okta.actor.alternate_id': { + category: 'okta', + description: 'Alternate identifier of the actor. ', + name: 'okta.actor.alternate_id', + type: 'keyword', + }, + 'okta.actor.display_name': { + category: 'okta', + description: 'Display name of the actor. ', + name: 'okta.actor.display_name', + type: 'keyword', + }, + 'okta.client.ip': { + category: 'okta', + description: 'The IP address of the client. ', + name: 'okta.client.ip', + type: 'ip', + }, + 'okta.client.user_agent.raw_user_agent': { + category: 'okta', + description: 'The raw informaton of the user agent. ', + name: 'okta.client.user_agent.raw_user_agent', + type: 'keyword', + }, + 'okta.client.user_agent.os': { + category: 'okta', + description: 'The OS informaton. ', + name: 'okta.client.user_agent.os', + type: 'keyword', + }, + 'okta.client.user_agent.browser': { + category: 'okta', + description: 'The browser informaton of the client. ', + name: 'okta.client.user_agent.browser', + type: 'keyword', + }, + 'okta.client.zone': { + category: 'okta', + description: 'The zone information of the client. ', + name: 'okta.client.zone', + type: 'keyword', + }, + 'okta.client.device': { + category: 'okta', + description: 'The information of the client device. ', + name: 'okta.client.device', + type: 'keyword', + }, + 'okta.client.id': { + category: 'okta', + description: 'The identifier of the client. ', + name: 'okta.client.id', + type: 'keyword', + }, + 'okta.outcome.reason': { + category: 'okta', + description: 'The reason of the outcome. ', + name: 'okta.outcome.reason', + type: 'keyword', + }, + 'okta.outcome.result': { + category: 'okta', + description: + 'The result of the outcome. Must be one of: SUCCESS, FAILURE, SKIPPED, ALLOW, DENY, CHALLENGE, UNKNOWN. ', + name: 'okta.outcome.result', + type: 'keyword', + }, + 'okta.target.id': { + category: 'okta', + description: 'Identifier of the actor. ', + name: 'okta.target.id', + type: 'keyword', + }, + 'okta.target.type': { + category: 'okta', + description: 'Type of the actor. ', + name: 'okta.target.type', + type: 'keyword', + }, + 'okta.target.alternate_id': { + category: 'okta', + description: 'Alternate identifier of the actor. ', + name: 'okta.target.alternate_id', + type: 'keyword', + }, + 'okta.target.display_name': { + category: 'okta', + description: 'Display name of the actor. ', + name: 'okta.target.display_name', + type: 'keyword', + }, + 'okta.transaction.id': { + category: 'okta', + description: 'Identifier of the transaction. ', + name: 'okta.transaction.id', + type: 'keyword', + }, + 'okta.transaction.type': { + category: 'okta', + description: 'The type of transaction. Must be one of "WEB", "JOB". ', + name: 'okta.transaction.type', + type: 'keyword', + }, + 'okta.debug_context.debug_data.device_fingerprint': { + category: 'okta', + description: 'The fingerprint of the device. ', + name: 'okta.debug_context.debug_data.device_fingerprint', + type: 'keyword', + }, + 'okta.debug_context.debug_data.request_id': { + category: 'okta', + description: 'The identifier of the request. ', + name: 'okta.debug_context.debug_data.request_id', + type: 'keyword', + }, + 'okta.debug_context.debug_data.request_uri': { + category: 'okta', + description: 'The request URI. ', + name: 'okta.debug_context.debug_data.request_uri', + type: 'keyword', + }, + 'okta.debug_context.debug_data.threat_suspected': { + category: 'okta', + description: 'Threat suspected. ', + name: 'okta.debug_context.debug_data.threat_suspected', + type: 'keyword', + }, + 'okta.debug_context.debug_data.url': { + category: 'okta', + description: 'The URL. ', + name: 'okta.debug_context.debug_data.url', + type: 'keyword', + }, + 'okta.authentication_context.authentication_provider': { + category: 'okta', + description: + 'The information about the authentication provider. Must be one of OKTA_AUTHENTICATION_PROVIDER, ACTIVE_DIRECTORY, LDAP, FEDERATION, SOCIAL, FACTOR_PROVIDER. ', + name: 'okta.authentication_context.authentication_provider', + type: 'keyword', + }, + 'okta.authentication_context.authentication_step': { + category: 'okta', + description: 'The authentication step. ', + name: 'okta.authentication_context.authentication_step', + type: 'integer', + }, + 'okta.authentication_context.credential_provider': { + category: 'okta', + description: + 'The information about credential provider. Must be one of OKTA_CREDENTIAL_PROVIDER, RSA, SYMANTEC, GOOGLE, DUO, YUBIKEY. ', + name: 'okta.authentication_context.credential_provider', + type: 'keyword', + }, + 'okta.authentication_context.credential_type': { + category: 'okta', + description: + 'The information about credential type. Must be one of OTP, SMS, PASSWORD, ASSERTION, IWA, EMAIL, OAUTH2, JWT, CERTIFICATE, PRE_SHARED_SYMMETRIC_KEY, OKTA_CLIENT_SESSION, DEVICE_UDID. ', + name: 'okta.authentication_context.credential_type', + type: 'keyword', + }, + 'okta.authentication_context.issuer.id': { + category: 'okta', + description: 'The identifier of the issuer. ', + name: 'okta.authentication_context.issuer.id', + type: 'keyword', + }, + 'okta.authentication_context.issuer.type': { + category: 'okta', + description: 'The type of the issuer. ', + name: 'okta.authentication_context.issuer.type', + type: 'keyword', + }, + 'okta.authentication_context.external_session_id': { + category: 'okta', + description: 'The session identifer of the external session if any. ', + name: 'okta.authentication_context.external_session_id', + type: 'keyword', + }, + 'okta.authentication_context.interface': { + category: 'okta', + description: 'The interface used. e.g., Outlook, Office365, wsTrust ', + name: 'okta.authentication_context.interface', + type: 'keyword', + }, + 'okta.security_context.as.number': { + category: 'okta', + description: 'The AS number. ', + name: 'okta.security_context.as.number', + type: 'integer', + }, + 'okta.security_context.as.organization.name': { + category: 'okta', + description: 'The organization name. ', + name: 'okta.security_context.as.organization.name', + type: 'keyword', + }, + 'okta.security_context.isp': { + category: 'okta', + description: 'The Internet Service Provider. ', + name: 'okta.security_context.isp', + type: 'keyword', + }, + 'okta.security_context.domain': { + category: 'okta', + description: 'The domain name. ', + name: 'okta.security_context.domain', + type: 'keyword', + }, + 'okta.security_context.is_proxy': { + category: 'okta', + description: 'Whether it is a proxy or not. ', + name: 'okta.security_context.is_proxy', + type: 'boolean', + }, + 'okta.request.ip_chain.ip': { + category: 'okta', + description: 'IP address. ', + name: 'okta.request.ip_chain.ip', + type: 'ip', + }, + 'okta.request.ip_chain.version': { + category: 'okta', + description: 'IP version. Must be one of V4, V6. ', + name: 'okta.request.ip_chain.version', + type: 'keyword', + }, + 'okta.request.ip_chain.source': { + category: 'okta', + description: 'Source information. ', + name: 'okta.request.ip_chain.source', + type: 'keyword', + }, + 'okta.request.ip_chain.geographical_context.city': { + category: 'okta', + description: 'The city.', + name: 'okta.request.ip_chain.geographical_context.city', + type: 'keyword', + }, + 'okta.request.ip_chain.geographical_context.state': { + category: 'okta', + description: 'The state.', + name: 'okta.request.ip_chain.geographical_context.state', + type: 'keyword', + }, + 'okta.request.ip_chain.geographical_context.postal_code': { + category: 'okta', + description: 'The postal code.', + name: 'okta.request.ip_chain.geographical_context.postal_code', + type: 'keyword', + }, + 'okta.request.ip_chain.geographical_context.country': { + category: 'okta', + description: 'The country.', + name: 'okta.request.ip_chain.geographical_context.country', + type: 'keyword', + }, + 'okta.request.ip_chain.geographical_context.geolocation': { + category: 'okta', + description: 'Geolocation information. ', + name: 'okta.request.ip_chain.geographical_context.geolocation', + type: 'geo_point', + }, + 'panw.panos.ruleset': { + category: 'panw', + description: 'Name of the rule that matched this session. ', + name: 'panw.panos.ruleset', + type: 'keyword', + }, + 'panw.panos.source.zone': { + category: 'panw', + description: 'Source zone for this session. ', + name: 'panw.panos.source.zone', + type: 'keyword', + }, + 'panw.panos.source.interface': { + category: 'panw', + description: 'Source interface for this session. ', + name: 'panw.panos.source.interface', + type: 'keyword', + }, + 'panw.panos.source.nat.ip': { + category: 'panw', + description: 'Post-NAT source IP. ', + name: 'panw.panos.source.nat.ip', + type: 'ip', + }, + 'panw.panos.source.nat.port': { + category: 'panw', + description: 'Post-NAT source port. ', + name: 'panw.panos.source.nat.port', + type: 'long', + }, + 'panw.panos.destination.zone': { + category: 'panw', + description: 'Destination zone for this session. ', + name: 'panw.panos.destination.zone', + type: 'keyword', + }, + 'panw.panos.destination.interface': { + category: 'panw', + description: 'Destination interface for this session. ', + name: 'panw.panos.destination.interface', + type: 'keyword', + }, + 'panw.panos.destination.nat.ip': { + category: 'panw', + description: 'Post-NAT destination IP. ', + name: 'panw.panos.destination.nat.ip', + type: 'ip', + }, + 'panw.panos.destination.nat.port': { + category: 'panw', + description: 'Post-NAT destination port. ', + name: 'panw.panos.destination.nat.port', + type: 'long', + }, + 'panw.panos.network.pcap_id': { + category: 'panw', + description: 'Packet capture ID for a threat. ', + name: 'panw.panos.network.pcap_id', + type: 'keyword', + }, + 'panw.panos.network.nat.community_id': { + category: 'panw', + description: 'Community ID flow-hash for the NAT 5-tuple. ', + name: 'panw.panos.network.nat.community_id', + type: 'keyword', + }, + 'panw.panos.file.hash': { + category: 'panw', + description: 'Binary hash for a threat file sent to be analyzed by the WildFire service. ', + name: 'panw.panos.file.hash', + type: 'keyword', + }, + 'panw.panos.url.category': { + category: 'panw', + description: + "For threat URLs, it's the URL category. For WildFire, the verdict on the file and is either 'malicious', 'grayware', or 'benign'. ", + name: 'panw.panos.url.category', + type: 'keyword', + }, + 'panw.panos.flow_id': { + category: 'panw', + description: 'Internal numeric identifier for each session. ', + name: 'panw.panos.flow_id', + type: 'keyword', + }, + 'panw.panos.sequence_number': { + category: 'panw', + description: + 'Log entry identifier that is incremented sequentially. Unique for each log type. ', + name: 'panw.panos.sequence_number', + type: 'long', + }, + 'panw.panos.threat.resource': { + category: 'panw', + description: 'URL or file name for a threat. ', + name: 'panw.panos.threat.resource', + type: 'keyword', + }, + 'panw.panos.threat.id': { + category: 'panw', + description: 'Palo Alto Networks identifier for the threat. ', + name: 'panw.panos.threat.id', + type: 'keyword', + }, + 'panw.panos.threat.name': { + category: 'panw', + description: 'Palo Alto Networks name for the threat. ', + name: 'panw.panos.threat.name', + type: 'keyword', + }, + 'panw.panos.action': { + category: 'panw', + description: 'Action taken for the session.', + name: 'panw.panos.action', + type: 'keyword', + }, + 'rabbitmq.log.pid': { + category: 'rabbitmq', + description: 'The Erlang process id', + example: '<0.222.0>', + name: 'rabbitmq.log.pid', + type: 'keyword', + }, + 'sophos.xg.device': { + category: 'sophos', + description: 'device ', + name: 'sophos.xg.device', + type: 'keyword', + }, + 'sophos.xg.date': { + category: 'sophos', + description: 'Date (yyyy-mm-dd) when the event occurred ', + name: 'sophos.xg.date', + type: 'date', + }, + 'sophos.xg.timezone': { + category: 'sophos', + description: 'Time (hh:mm:ss) when the event occurred ', + name: 'sophos.xg.timezone', + type: 'keyword', + }, + 'sophos.xg.device_name': { + category: 'sophos', + description: 'Model number of the device ', + name: 'sophos.xg.device_name', + type: 'keyword', + }, + 'sophos.xg.device_id': { + category: 'sophos', + description: 'Serial number of the device ', + name: 'sophos.xg.device_id', + type: 'keyword', + }, + 'sophos.xg.log_id': { + category: 'sophos', + description: 'Unique 12 characters code (0101011) ', + name: 'sophos.xg.log_id', + type: 'keyword', + }, + 'sophos.xg.log_type': { + category: 'sophos', + description: 'Type of event e.g. firewall event ', + name: 'sophos.xg.log_type', + type: 'keyword', + }, + 'sophos.xg.log_component': { + category: 'sophos', + description: 'Component responsible for logging e.g. Firewall rule ', + name: 'sophos.xg.log_component', + type: 'keyword', + }, + 'sophos.xg.log_subtype': { + category: 'sophos', + description: 'Sub type of event ', + name: 'sophos.xg.log_subtype', + type: 'keyword', + }, + 'sophos.xg.hb_health': { + category: 'sophos', + description: 'Heartbeat status ', + name: 'sophos.xg.hb_health', + type: 'keyword', + }, + 'sophos.xg.priority': { + category: 'sophos', + description: 'Severity level of traffic ', + name: 'sophos.xg.priority', + type: 'keyword', + }, + 'sophos.xg.status': { + category: 'sophos', + description: 'Ultimate status of traffic – Allowed or Denied ', + name: 'sophos.xg.status', + type: 'keyword', + }, + 'sophos.xg.duration': { + category: 'sophos', + description: 'Durability of traffic (seconds) ', + name: 'sophos.xg.duration', + type: 'long', + }, + 'sophos.xg.fw_rule_id': { + category: 'sophos', + description: 'Firewall Rule ID which is applied on the traffic ', + name: 'sophos.xg.fw_rule_id', + type: 'integer', + }, + 'sophos.xg.user_name': { + category: 'sophos', + description: 'user_name ', + name: 'sophos.xg.user_name', + type: 'keyword', + }, + 'sophos.xg.user_group': { + category: 'sophos', + description: 'Group name to which the user belongs ', + name: 'sophos.xg.user_group', + type: 'keyword', + }, + 'sophos.xg.iap': { + category: 'sophos', + description: 'Internet Access policy ID applied on the traffic ', + name: 'sophos.xg.iap', + type: 'keyword', + }, + 'sophos.xg.ips_policy_id': { + category: 'sophos', + description: 'IPS policy ID applied on the traffic ', + name: 'sophos.xg.ips_policy_id', + type: 'integer', + }, + 'sophos.xg.policy_type': { + category: 'sophos', + description: 'Policy type applied to the traffic ', + name: 'sophos.xg.policy_type', + type: 'keyword', + }, + 'sophos.xg.appfilter_policy_id': { + category: 'sophos', + description: 'Application Filter policy applied on the traffic ', + name: 'sophos.xg.appfilter_policy_id', + type: 'integer', + }, + 'sophos.xg.application_filter_policy': { + category: 'sophos', + description: 'Application Filter policy applied on the traffic ', + name: 'sophos.xg.application_filter_policy', + type: 'integer', + }, + 'sophos.xg.application': { + category: 'sophos', + description: 'Application name ', + name: 'sophos.xg.application', + type: 'keyword', + }, + 'sophos.xg.application_name': { + category: 'sophos', + description: 'Application name ', + name: 'sophos.xg.application_name', + type: 'keyword', + }, + 'sophos.xg.application_risk': { + category: 'sophos', + description: 'Risk level assigned to the application ', + name: 'sophos.xg.application_risk', + type: 'keyword', + }, + 'sophos.xg.application_technology': { + category: 'sophos', + description: 'Technology of the application ', + name: 'sophos.xg.application_technology', + type: 'keyword', + }, + 'sophos.xg.application_category': { + category: 'sophos', + description: 'Application is resolved by signature or synchronized application ', + name: 'sophos.xg.application_category', + type: 'keyword', + }, + 'sophos.xg.appresolvedby': { + category: 'sophos', + description: 'Technology of the application ', + name: 'sophos.xg.appresolvedby', + type: 'keyword', + }, + 'sophos.xg.app_is_cloud': { + category: 'sophos', + description: 'Application is Cloud ', + name: 'sophos.xg.app_is_cloud', + type: 'keyword', + }, + 'sophos.xg.in_interface': { + category: 'sophos', + description: 'Interface for incoming traffic, e.g., Port A ', + name: 'sophos.xg.in_interface', + type: 'keyword', + }, + 'sophos.xg.out_interface': { + category: 'sophos', + description: 'Interface for outgoing traffic, e.g., Port B ', + name: 'sophos.xg.out_interface', + type: 'keyword', + }, + 'sophos.xg.src_ip': { + category: 'sophos', + description: 'Original source IP address of traffic ', + name: 'sophos.xg.src_ip', + type: 'ip', + }, + 'sophos.xg.src_mac': { + category: 'sophos', + description: 'Original source MAC address of traffic ', + name: 'sophos.xg.src_mac', + type: 'keyword', + }, + 'sophos.xg.src_country_code': { + category: 'sophos', + description: 'Code of the country to which the source IP belongs ', + name: 'sophos.xg.src_country_code', + type: 'keyword', + }, + 'sophos.xg.dst_ip': { + category: 'sophos', + description: 'Original destination IP address of traffic ', + name: 'sophos.xg.dst_ip', + type: 'ip', + }, + 'sophos.xg.dst_country_code': { + category: 'sophos', + description: 'Code of the country to which the destination IP belongs ', + name: 'sophos.xg.dst_country_code', + type: 'keyword', + }, + 'sophos.xg.protocol': { + category: 'sophos', + description: 'Protocol number of traffic ', + name: 'sophos.xg.protocol', + type: 'keyword', + }, + 'sophos.xg.src_port': { + category: 'sophos', + description: 'Original source port of TCP and UDP traffic ', + name: 'sophos.xg.src_port', + type: 'integer', + }, + 'sophos.xg.dst_port': { + category: 'sophos', + description: 'Original destination port of TCP and UDP traffic ', + name: 'sophos.xg.dst_port', + type: 'integer', + }, + 'sophos.xg.icmp_type': { + category: 'sophos', + description: 'ICMP type of ICMP traffic ', + name: 'sophos.xg.icmp_type', + type: 'keyword', + }, + 'sophos.xg.icmp_code': { + category: 'sophos', + description: 'ICMP code of ICMP traffic ', + name: 'sophos.xg.icmp_code', + type: 'keyword', + }, + 'sophos.xg.sent_pkts': { + category: 'sophos', + description: 'Total number of packets sent ', + name: 'sophos.xg.sent_pkts', + type: 'long', + }, + 'sophos.xg.received_pkts': { + category: 'sophos', + description: 'Total number of packets received ', + name: 'sophos.xg.received_pkts', + type: 'long', + }, + 'sophos.xg.sent_bytes': { + category: 'sophos', + description: 'Total number of bytes sent ', + name: 'sophos.xg.sent_bytes', + type: 'long', + }, + 'sophos.xg.recv_bytes': { + category: 'sophos', + description: 'Total number of bytes received ', + name: 'sophos.xg.recv_bytes', + type: 'long', + }, + 'sophos.xg.trans_src_ ip': { + category: 'sophos', + description: 'Translated source IP address for outgoing traffic ', + name: 'sophos.xg.trans_src_ ip', + type: 'ip', + }, + 'sophos.xg.trans_src_port': { + category: 'sophos', + description: 'Translated source port for outgoing traffic ', + name: 'sophos.xg.trans_src_port', + type: 'integer', + }, + 'sophos.xg.trans_dst_ip': { + category: 'sophos', + description: 'Translated destination IP address for outgoing traffic ', + name: 'sophos.xg.trans_dst_ip', + type: 'ip', + }, + 'sophos.xg.trans_dst_port': { + category: 'sophos', + description: 'Translated destination port for outgoing traffic ', + name: 'sophos.xg.trans_dst_port', + type: 'integer', + }, + 'sophos.xg.srczonetype': { + category: 'sophos', + description: 'Type of source zone, e.g., LAN ', + name: 'sophos.xg.srczonetype', + type: 'keyword', + }, + 'sophos.xg.srczone': { + category: 'sophos', + description: 'Name of source zone ', + name: 'sophos.xg.srczone', + type: 'keyword', + }, + 'sophos.xg.dstzonetype': { + category: 'sophos', + description: 'Type of destination zone, e.g., WAN ', + name: 'sophos.xg.dstzonetype', + type: 'keyword', + }, + 'sophos.xg.dstzone': { + category: 'sophos', + description: 'Name of destination zone ', + name: 'sophos.xg.dstzone', + type: 'keyword', + }, + 'sophos.xg.dir_disp': { + category: 'sophos', + description: 'TPacket direction. Possible values:“org”, “reply”, “” ', + name: 'sophos.xg.dir_disp', + type: 'keyword', + }, + 'sophos.xg.connevent': { + category: 'sophos', + description: 'Event on which this log is generated ', + name: 'sophos.xg.connevent', + type: 'keyword', + }, + 'sophos.xg.conn_id': { + category: 'sophos', + description: 'Unique identifier of connection ', + name: 'sophos.xg.conn_id', + type: 'integer', + }, + 'sophos.xg.vconn_id': { + category: 'sophos', + description: 'Connection ID of the master connection ', + name: 'sophos.xg.vconn_id', + type: 'integer', + }, + 'sophos.xg.idp_policy_id': { + category: 'sophos', + description: 'IPS policy ID which is applied on the traffic ', + name: 'sophos.xg.idp_policy_id', + type: 'integer', + }, + 'sophos.xg.idp_policy_name': { + category: 'sophos', + description: 'IPS policy name i.e. IPS policy name which is applied on the traffic ', + name: 'sophos.xg.idp_policy_name', + type: 'keyword', + }, + 'sophos.xg.signature_id': { + category: 'sophos', + description: 'Signature ID ', + name: 'sophos.xg.signature_id', + type: 'keyword', + }, + 'sophos.xg.signature_msg': { + category: 'sophos', + description: 'Signature messsage ', + name: 'sophos.xg.signature_msg', + type: 'keyword', + }, + 'sophos.xg.classification': { + category: 'sophos', + description: 'Signature classification ', + name: 'sophos.xg.classification', + type: 'keyword', + }, + 'sophos.xg.rule_priority': { + category: 'sophos', + description: 'Priority of IPS policy ', + name: 'sophos.xg.rule_priority', + type: 'keyword', + }, + 'sophos.xg.platform': { + category: 'sophos', + description: 'Platform of the traffic. ', + name: 'sophos.xg.platform', + type: 'keyword', + }, + 'sophos.xg.category': { + category: 'sophos', + description: 'IPS signature category. ', + name: 'sophos.xg.category', + type: 'keyword', + }, + 'sophos.xg.target': { + category: 'sophos', + description: 'Platform of the traffic. ', + name: 'sophos.xg.target', + type: 'keyword', + }, + 'sophos.xg.eventid': { + category: 'sophos', + description: 'ATP Evenet ID ', + name: 'sophos.xg.eventid', + type: 'keyword', + }, + 'sophos.xg.ep_uuid': { + category: 'sophos', + description: 'Endpoint UUID ', + name: 'sophos.xg.ep_uuid', + type: 'keyword', + }, + 'sophos.xg.threatname': { + category: 'sophos', + description: 'ATP threatname ', + name: 'sophos.xg.threatname', + type: 'keyword', + }, + 'sophos.xg.sourceip': { + category: 'sophos', + description: 'Original source IP address of traffic ', + name: 'sophos.xg.sourceip', + type: 'ip', + }, + 'sophos.xg.destinationip': { + category: 'sophos', + description: 'Original destination IP address of traffic ', + name: 'sophos.xg.destinationip', + type: 'ip', + }, + 'sophos.xg.login_user': { + category: 'sophos', + description: 'ATP login user ', + name: 'sophos.xg.login_user', + type: 'keyword', + }, + 'sophos.xg.eventtype': { + category: 'sophos', + description: 'ATP event type ', + name: 'sophos.xg.eventtype', + type: 'keyword', + }, + 'sophos.xg.execution_path': { + category: 'sophos', + description: 'ATP execution path ', + name: 'sophos.xg.execution_path', + type: 'keyword', + }, + 'sophos.xg.av_policy_name': { + category: 'sophos', + description: 'Malware scanning policy name which is applied on the traffic ', + name: 'sophos.xg.av_policy_name', + type: 'keyword', + }, + 'sophos.xg.from_email_address': { + category: 'sophos', + description: 'Sender email address ', + name: 'sophos.xg.from_email_address', + type: 'keyword', + }, + 'sophos.xg.to_email_address': { + category: 'sophos', + description: 'Receipeint email address ', + name: 'sophos.xg.to_email_address', + type: 'keyword', + }, + 'sophos.xg.subject': { + category: 'sophos', + description: 'Email subject ', + name: 'sophos.xg.subject', + type: 'keyword', + }, + 'sophos.xg.mailsize': { + category: 'sophos', + description: 'mailsize ', + name: 'sophos.xg.mailsize', + type: 'integer', + }, + 'sophos.xg.virus': { + category: 'sophos', + description: 'virus name ', + name: 'sophos.xg.virus', + type: 'keyword', + }, + 'sophos.xg.FTP_url': { + category: 'sophos', + description: 'FTP URL from which virus was downloaded ', + name: 'sophos.xg.FTP_url', + type: 'keyword', + }, + 'sophos.xg.FTP_direction': { + category: 'sophos', + description: 'Direction of FTP transfer: Upload or Download ', + name: 'sophos.xg.FTP_direction', + type: 'keyword', + }, + 'sophos.xg.filesize': { + category: 'sophos', + description: 'Size of the file that contained virus ', + name: 'sophos.xg.filesize', + type: 'integer', + }, + 'sophos.xg.filepath': { + category: 'sophos', + description: 'Path of the file containing virus ', + name: 'sophos.xg.filepath', + type: 'keyword', + }, + 'sophos.xg.filename': { + category: 'sophos', + description: 'File name associated with the event ', + name: 'sophos.xg.filename', + type: 'keyword', + }, + 'sophos.xg.ftpcommand': { + category: 'sophos', + description: 'FTP command used when virus was found ', + name: 'sophos.xg.ftpcommand', + type: 'keyword', + }, + 'sophos.xg.url': { + category: 'sophos', + description: 'URL from which virus was downloaded ', + name: 'sophos.xg.url', + type: 'keyword', + }, + 'sophos.xg.domainname': { + category: 'sophos', + description: 'Domain from which virus was downloaded ', + name: 'sophos.xg.domainname', + type: 'keyword', + }, + 'sophos.xg.quarantine': { + category: 'sophos', + description: 'Path and filename of the file quarantined ', + name: 'sophos.xg.quarantine', + type: 'keyword', + }, + 'sophos.xg.src_domainname': { + category: 'sophos', + description: 'Sender domain name ', + name: 'sophos.xg.src_domainname', + type: 'keyword', + }, + 'sophos.xg.dst_domainname': { + category: 'sophos', + description: 'Receiver domain name ', + name: 'sophos.xg.dst_domainname', + type: 'keyword', + }, + 'sophos.xg.reason': { + category: 'sophos', + description: 'Reason why the record was detected as spam/malicious ', + name: 'sophos.xg.reason', + type: 'keyword', + }, + 'sophos.xg.referer': { + category: 'sophos', + description: 'Referer ', + name: 'sophos.xg.referer', + type: 'keyword', + }, + 'sophos.xg.spamaction': { + category: 'sophos', + description: 'Spam Action ', + name: 'sophos.xg.spamaction', + type: 'keyword', + }, + 'sophos.xg.mailid': { + category: 'sophos', + description: 'mailid ', + name: 'sophos.xg.mailid', + type: 'keyword', + }, + 'sophos.xg.quarantine_reason': { + category: 'sophos', + description: 'Quarantine reason ', + name: 'sophos.xg.quarantine_reason', + type: 'keyword', + }, + 'sophos.xg.status_code': { + category: 'sophos', + description: 'Status code ', + name: 'sophos.xg.status_code', + type: 'keyword', + }, + 'sophos.xg.override_token': { + category: 'sophos', + description: 'Override token ', + name: 'sophos.xg.override_token', + type: 'keyword', + }, + 'sophos.xg.con_id': { + category: 'sophos', + description: 'Unique identifier of connection ', + name: 'sophos.xg.con_id', + type: 'integer', + }, + 'sophos.xg.override_authorizer': { + category: 'sophos', + description: 'Override authorizer ', + name: 'sophos.xg.override_authorizer', + type: 'keyword', + }, + 'sophos.xg.transactionid': { + category: 'sophos', + description: 'Transaction ID of the AV scan. ', + name: 'sophos.xg.transactionid', + type: 'keyword', + }, + 'sophos.xg.upload_file_type': { + category: 'sophos', + description: 'Upload file type ', + name: 'sophos.xg.upload_file_type', + type: 'keyword', + }, + 'sophos.xg.upload_file_name': { + category: 'sophos', + description: 'Upload file name ', + name: 'sophos.xg.upload_file_name', + type: 'keyword', + }, + 'sophos.xg.httpresponsecode': { + category: 'sophos', + description: 'code of HTTP response ', + name: 'sophos.xg.httpresponsecode', + type: 'long', + }, + 'sophos.xg.user_gp': { + category: 'sophos', + description: 'Group name to which the user belongs. ', + name: 'sophos.xg.user_gp', + type: 'keyword', + }, + 'sophos.xg.category_type': { + category: 'sophos', + description: 'Type of category under which website falls ', + name: 'sophos.xg.category_type', + type: 'keyword', + }, + 'sophos.xg.download_file_type': { + category: 'sophos', + description: 'Download file type ', + name: 'sophos.xg.download_file_type', + type: 'keyword', + }, + 'sophos.xg.exceptions': { + category: 'sophos', + description: 'List of the checks excluded by web exceptions. ', + name: 'sophos.xg.exceptions', + type: 'keyword', + }, + 'sophos.xg.contenttype': { + category: 'sophos', + description: 'Type of the content ', + name: 'sophos.xg.contenttype', + type: 'keyword', + }, + 'sophos.xg.override_name': { + category: 'sophos', + description: 'Override name ', + name: 'sophos.xg.override_name', + type: 'keyword', + }, + 'sophos.xg.activityname': { + category: 'sophos', + description: 'Web policy activity that matched and caused the policy result. ', + name: 'sophos.xg.activityname', + type: 'keyword', + }, + 'sophos.xg.download_file_name': { + category: 'sophos', + description: 'Download file name ', + name: 'sophos.xg.download_file_name', + type: 'keyword', + }, + 'sophos.xg.sha1sum': { + category: 'sophos', + description: 'SHA1 checksum of the item being analyzed ', + name: 'sophos.xg.sha1sum', + type: 'keyword', + }, + 'sophos.xg.message_id': { + category: 'sophos', + description: 'Message ID ', + name: 'sophos.xg.message_id', + type: 'keyword', + }, + 'sophos.xg.connid': { + category: 'sophos', + description: 'Connection ID ', + name: 'sophos.xg.connid', + type: 'keyword', + }, + 'sophos.xg.message': { + category: 'sophos', + description: 'Message ', + name: 'sophos.xg.message', + type: 'keyword', + }, + 'sophos.xg.email_subject': { + category: 'sophos', + description: 'Email Subject ', + name: 'sophos.xg.email_subject', + type: 'keyword', + }, + 'sophos.xg.file_path': { + category: 'sophos', + description: 'File path ', + name: 'sophos.xg.file_path', + type: 'keyword', + }, + 'sophos.xg.dstdomain': { + category: 'sophos', + description: 'Destination Domain ', + name: 'sophos.xg.dstdomain', + type: 'keyword', + }, + 'sophos.xg.file_size': { + category: 'sophos', + description: 'File Size ', + name: 'sophos.xg.file_size', + type: 'integer', + }, + 'sophos.xg.transaction_id': { + category: 'sophos', + description: 'Transaction ID ', + name: 'sophos.xg.transaction_id', + type: 'keyword', + }, + 'sophos.xg.website': { + category: 'sophos', + description: 'Website ', + name: 'sophos.xg.website', + type: 'keyword', + }, + 'sophos.xg.file_name': { + category: 'sophos', + description: 'Filename ', + name: 'sophos.xg.file_name', + type: 'keyword', + }, + 'sophos.xg.context_prefix': { + category: 'sophos', + description: 'Content Prefix ', + name: 'sophos.xg.context_prefix', + type: 'keyword', + }, + 'sophos.xg.site_category': { + category: 'sophos', + description: 'Site Category ', + name: 'sophos.xg.site_category', + type: 'keyword', + }, + 'sophos.xg.context_suffix': { + category: 'sophos', + description: 'Context Suffix ', + name: 'sophos.xg.context_suffix', + type: 'keyword', + }, + 'sophos.xg.dictionary_name': { + category: 'sophos', + description: 'Dictionary Name ', + name: 'sophos.xg.dictionary_name', + type: 'keyword', + }, + 'sophos.xg.action': { + category: 'sophos', + description: 'Event Action ', + name: 'sophos.xg.action', + type: 'keyword', + }, + 'sophos.xg.user': { + category: 'sophos', + description: 'User ', + name: 'sophos.xg.user', + type: 'keyword', + }, + 'sophos.xg.context_match': { + category: 'sophos', + description: 'Context Match ', + name: 'sophos.xg.context_match', + type: 'keyword', + }, + 'sophos.xg.direction': { + category: 'sophos', + description: 'Direction ', + name: 'sophos.xg.direction', + type: 'keyword', + }, + 'sophos.xg.auth_client': { + category: 'sophos', + description: 'Auth Client ', + name: 'sophos.xg.auth_client', + type: 'keyword', + }, + 'sophos.xg.auth_mechanism': { + category: 'sophos', + description: 'Auth mechanism ', + name: 'sophos.xg.auth_mechanism', + type: 'keyword', + }, + 'sophos.xg.connectionname': { + category: 'sophos', + description: 'Connectionname ', + name: 'sophos.xg.connectionname', + type: 'keyword', + }, + 'sophos.xg.remotenetwork': { + category: 'sophos', + description: 'remotenetwork ', + name: 'sophos.xg.remotenetwork', + type: 'keyword', + }, + 'sophos.xg.localgateway': { + category: 'sophos', + description: 'Localgateway ', + name: 'sophos.xg.localgateway', + type: 'keyword', + }, + 'sophos.xg.localnetwork': { + category: 'sophos', + description: 'Localnetwork ', + name: 'sophos.xg.localnetwork', + type: 'keyword', + }, + 'sophos.xg.connectiontype': { + category: 'sophos', + description: 'Connectiontype ', + name: 'sophos.xg.connectiontype', + type: 'keyword', + }, + 'sophos.xg.oldversion': { + category: 'sophos', + description: 'Oldversion ', + name: 'sophos.xg.oldversion', + type: 'keyword', + }, + 'sophos.xg.newversion': { + category: 'sophos', + description: 'Newversion ', + name: 'sophos.xg.newversion', + type: 'keyword', + }, + 'sophos.xg.ipaddress': { + category: 'sophos', + description: 'Ipaddress ', + name: 'sophos.xg.ipaddress', + type: 'keyword', + }, + 'sophos.xg.client_physical_address': { + category: 'sophos', + description: 'Client physical address ', + name: 'sophos.xg.client_physical_address', + type: 'keyword', + }, + 'sophos.xg.client_host_name': { + category: 'sophos', + description: 'Client host name ', + name: 'sophos.xg.client_host_name', + type: 'keyword', + }, + 'sophos.xg.raw_data': { + category: 'sophos', + description: 'Raw data ', + name: 'sophos.xg.raw_data', + type: 'keyword', + }, + 'sophos.xg.Mode': { + category: 'sophos', + description: 'Mode ', + name: 'sophos.xg.Mode', + type: 'keyword', + }, + 'sophos.xg.sessionid': { + category: 'sophos', + description: 'Sessionid ', + name: 'sophos.xg.sessionid', + type: 'keyword', + }, + 'sophos.xg.starttime': { + category: 'sophos', + description: 'Starttime ', + name: 'sophos.xg.starttime', + type: 'date', + }, + 'sophos.xg.remote_ip': { + category: 'sophos', + description: 'Remote IP ', + name: 'sophos.xg.remote_ip', + type: 'ip', + }, + 'sophos.xg.timestamp': { + category: 'sophos', + description: 'timestamp ', + name: 'sophos.xg.timestamp', + type: 'date', + }, + 'sophos.xg.SysLog_SERVER_NAME': { + category: 'sophos', + description: 'SysLog SERVER NAME ', + name: 'sophos.xg.SysLog_SERVER_NAME', + type: 'keyword', + }, + 'sophos.xg.backup_mode': { + category: 'sophos', + description: 'Backup mode ', + name: 'sophos.xg.backup_mode', + type: 'keyword', + }, + 'sophos.xg.source': { + category: 'sophos', + description: 'Source ', + name: 'sophos.xg.source', + type: 'keyword', + }, + 'sophos.xg.server': { + category: 'sophos', + description: 'Server ', + name: 'sophos.xg.server', + type: 'keyword', + }, + 'sophos.xg.host': { + category: 'sophos', + description: 'Host ', + name: 'sophos.xg.host', + type: 'keyword', + }, + 'sophos.xg.responsetime': { + category: 'sophos', + description: 'Responsetime ', + name: 'sophos.xg.responsetime', + type: 'long', + }, + 'sophos.xg.cookie': { + category: 'sophos', + description: 'cookie ', + name: 'sophos.xg.cookie', + type: 'keyword', + }, + 'sophos.xg.querystring': { + category: 'sophos', + description: 'querystring ', + name: 'sophos.xg.querystring', + type: 'keyword', + }, + 'sophos.xg.extra': { + category: 'sophos', + description: 'extra ', + name: 'sophos.xg.extra', + type: 'keyword', + }, + 'sophos.xg.PHPSESSID': { + category: 'sophos', + description: 'PHPSESSID ', + name: 'sophos.xg.PHPSESSID', + type: 'keyword', + }, + 'sophos.xg.start_time': { + category: 'sophos', + description: 'Start time ', + name: 'sophos.xg.start_time', + type: 'date', + }, + 'sophos.xg.eventtime': { + category: 'sophos', + description: 'Event time ', + name: 'sophos.xg.eventtime', + type: 'date', + }, + 'sophos.xg.red_id': { + category: 'sophos', + description: 'RED ID ', + name: 'sophos.xg.red_id', + type: 'keyword', + }, + 'sophos.xg.branch_name': { + category: 'sophos', + description: 'Branch Name ', + name: 'sophos.xg.branch_name', + type: 'keyword', + }, + 'sophos.xg.updatedip': { + category: 'sophos', + description: 'updatedip ', + name: 'sophos.xg.updatedip', + type: 'ip', + }, + 'sophos.xg.idle_cpu': { + category: 'sophos', + description: 'idle ## ', + name: 'sophos.xg.idle_cpu', + type: 'float', + }, + 'sophos.xg.system_cpu': { + category: 'sophos', + description: 'system ', + name: 'sophos.xg.system_cpu', + type: 'float', + }, + 'sophos.xg.user_cpu': { + category: 'sophos', + description: 'system ', + name: 'sophos.xg.user_cpu', + type: 'float', + }, + 'sophos.xg.used': { + category: 'sophos', + description: 'used ', + name: 'sophos.xg.used', + type: 'integer', + }, + 'sophos.xg.unit': { + category: 'sophos', + description: 'unit ', + name: 'sophos.xg.unit', + type: 'keyword', + }, + 'sophos.xg.total_memory': { + category: 'sophos', + description: 'Total Memory ', + name: 'sophos.xg.total_memory', + type: 'integer', + }, + 'sophos.xg.free': { + category: 'sophos', + description: 'free ', + name: 'sophos.xg.free', + type: 'integer', + }, + 'sophos.xg.transmittederrors': { + category: 'sophos', + description: 'transmitted errors ', + name: 'sophos.xg.transmittederrors', + type: 'keyword', + }, + 'sophos.xg.receivederrors': { + category: 'sophos', + description: 'received errors ', + name: 'sophos.xg.receivederrors', + type: 'keyword', + }, + 'sophos.xg.receivedkbits': { + category: 'sophos', + description: 'received kbits ', + name: 'sophos.xg.receivedkbits', + type: 'long', + }, + 'sophos.xg.transmittedkbits': { + category: 'sophos', + description: 'transmitted kbits ', + name: 'sophos.xg.transmittedkbits', + type: 'long', + }, + 'sophos.xg.transmitteddrops': { + category: 'sophos', + description: 'transmitted drops ', + name: 'sophos.xg.transmitteddrops', + type: 'long', + }, + 'sophos.xg.receiveddrops': { + category: 'sophos', + description: 'received drops ', + name: 'sophos.xg.receiveddrops', + type: 'long', + }, + 'sophos.xg.collisions': { + category: 'sophos', + description: 'collisions ', + name: 'sophos.xg.collisions', + type: 'long', + }, + 'sophos.xg.interface': { + category: 'sophos', + description: 'interface ', + name: 'sophos.xg.interface', + type: 'keyword', + }, + 'sophos.xg.Configuration': { + category: 'sophos', + description: 'Configuration ', + name: 'sophos.xg.Configuration', + type: 'float', + }, + 'sophos.xg.Reports': { + category: 'sophos', + description: 'Reports ', + name: 'sophos.xg.Reports', + type: 'float', + }, + 'sophos.xg.Signature': { + category: 'sophos', + description: 'Signature ', + name: 'sophos.xg.Signature', + type: 'float', + }, + 'sophos.xg.Temp': { + category: 'sophos', + description: 'Temp ', + name: 'sophos.xg.Temp', + type: 'float', + }, + 'sophos.xg.users': { + category: 'sophos', + description: 'users ', + name: 'sophos.xg.users', + type: 'keyword', + }, + 'sophos.xg.ssid': { + category: 'sophos', + description: 'ssid ', + name: 'sophos.xg.ssid', + type: 'keyword', + }, + 'sophos.xg.ap': { + category: 'sophos', + description: 'ap ', + name: 'sophos.xg.ap', + type: 'keyword', + }, + 'sophos.xg.clients_conn_ssid': { + category: 'sophos', + description: 'clients connection ssid ', + name: 'sophos.xg.clients_conn_ssid', + type: 'keyword', + }, + 'suricata.eve.event_type': { + category: 'suricata', + name: 'suricata.eve.event_type', + type: 'keyword', + }, + 'suricata.eve.app_proto_orig': { + category: 'suricata', + name: 'suricata.eve.app_proto_orig', + type: 'keyword', + }, + 'suricata.eve.tcp.tcp_flags': { + category: 'suricata', + name: 'suricata.eve.tcp.tcp_flags', + type: 'keyword', + }, + 'suricata.eve.tcp.psh': { + category: 'suricata', + name: 'suricata.eve.tcp.psh', + type: 'boolean', + }, + 'suricata.eve.tcp.tcp_flags_tc': { + category: 'suricata', + name: 'suricata.eve.tcp.tcp_flags_tc', + type: 'keyword', + }, + 'suricata.eve.tcp.ack': { + category: 'suricata', + name: 'suricata.eve.tcp.ack', + type: 'boolean', + }, + 'suricata.eve.tcp.syn': { + category: 'suricata', + name: 'suricata.eve.tcp.syn', + type: 'boolean', + }, + 'suricata.eve.tcp.state': { + category: 'suricata', + name: 'suricata.eve.tcp.state', + type: 'keyword', + }, + 'suricata.eve.tcp.tcp_flags_ts': { + category: 'suricata', + name: 'suricata.eve.tcp.tcp_flags_ts', + type: 'keyword', + }, + 'suricata.eve.tcp.rst': { + category: 'suricata', + name: 'suricata.eve.tcp.rst', + type: 'boolean', + }, + 'suricata.eve.tcp.fin': { + category: 'suricata', + name: 'suricata.eve.tcp.fin', + type: 'boolean', + }, + 'suricata.eve.fileinfo.sha1': { + category: 'suricata', + name: 'suricata.eve.fileinfo.sha1', + type: 'keyword', + }, + 'suricata.eve.fileinfo.filename': { + category: 'suricata', + name: 'suricata.eve.fileinfo.filename', + type: 'alias', + }, + 'suricata.eve.fileinfo.tx_id': { + category: 'suricata', + name: 'suricata.eve.fileinfo.tx_id', + type: 'long', + }, + 'suricata.eve.fileinfo.state': { + category: 'suricata', + name: 'suricata.eve.fileinfo.state', + type: 'keyword', + }, + 'suricata.eve.fileinfo.stored': { + category: 'suricata', + name: 'suricata.eve.fileinfo.stored', + type: 'boolean', + }, + 'suricata.eve.fileinfo.gaps': { + category: 'suricata', + name: 'suricata.eve.fileinfo.gaps', + type: 'boolean', + }, + 'suricata.eve.fileinfo.sha256': { + category: 'suricata', + name: 'suricata.eve.fileinfo.sha256', + type: 'keyword', + }, + 'suricata.eve.fileinfo.md5': { + category: 'suricata', + name: 'suricata.eve.fileinfo.md5', + type: 'keyword', + }, + 'suricata.eve.fileinfo.size': { + category: 'suricata', + name: 'suricata.eve.fileinfo.size', + type: 'alias', + }, + 'suricata.eve.icmp_type': { + category: 'suricata', + name: 'suricata.eve.icmp_type', + type: 'long', + }, + 'suricata.eve.dest_port': { + category: 'suricata', + name: 'suricata.eve.dest_port', + type: 'alias', + }, + 'suricata.eve.src_port': { + category: 'suricata', + name: 'suricata.eve.src_port', + type: 'alias', + }, + 'suricata.eve.proto': { + category: 'suricata', + name: 'suricata.eve.proto', + type: 'alias', + }, + 'suricata.eve.pcap_cnt': { + category: 'suricata', + name: 'suricata.eve.pcap_cnt', + type: 'long', + }, + 'suricata.eve.src_ip': { + category: 'suricata', + name: 'suricata.eve.src_ip', + type: 'alias', + }, + 'suricata.eve.dns.type': { + category: 'suricata', + name: 'suricata.eve.dns.type', + type: 'keyword', + }, + 'suricata.eve.dns.rrtype': { + category: 'suricata', + name: 'suricata.eve.dns.rrtype', + type: 'keyword', + }, + 'suricata.eve.dns.rrname': { + category: 'suricata', + name: 'suricata.eve.dns.rrname', + type: 'keyword', + }, + 'suricata.eve.dns.rdata': { + category: 'suricata', + name: 'suricata.eve.dns.rdata', + type: 'keyword', + }, + 'suricata.eve.dns.tx_id': { + category: 'suricata', + name: 'suricata.eve.dns.tx_id', + type: 'long', + }, + 'suricata.eve.dns.ttl': { + category: 'suricata', + name: 'suricata.eve.dns.ttl', + type: 'long', + }, + 'suricata.eve.dns.rcode': { + category: 'suricata', + name: 'suricata.eve.dns.rcode', + type: 'keyword', + }, + 'suricata.eve.dns.id': { + category: 'suricata', + name: 'suricata.eve.dns.id', + type: 'long', + }, + 'suricata.eve.flow_id': { + category: 'suricata', + name: 'suricata.eve.flow_id', + type: 'keyword', + }, + 'suricata.eve.email.status': { + category: 'suricata', + name: 'suricata.eve.email.status', + type: 'keyword', + }, + 'suricata.eve.dest_ip': { + category: 'suricata', + name: 'suricata.eve.dest_ip', + type: 'alias', + }, + 'suricata.eve.icmp_code': { + category: 'suricata', + name: 'suricata.eve.icmp_code', + type: 'long', + }, + 'suricata.eve.http.status': { + category: 'suricata', + name: 'suricata.eve.http.status', + type: 'alias', + }, + 'suricata.eve.http.redirect': { + category: 'suricata', + name: 'suricata.eve.http.redirect', + type: 'keyword', + }, + 'suricata.eve.http.http_user_agent': { + category: 'suricata', + name: 'suricata.eve.http.http_user_agent', + type: 'alias', + }, + 'suricata.eve.http.protocol': { + category: 'suricata', + name: 'suricata.eve.http.protocol', + type: 'keyword', + }, + 'suricata.eve.http.http_refer': { + category: 'suricata', + name: 'suricata.eve.http.http_refer', + type: 'alias', + }, + 'suricata.eve.http.url': { + category: 'suricata', + name: 'suricata.eve.http.url', + type: 'alias', + }, + 'suricata.eve.http.hostname': { + category: 'suricata', + name: 'suricata.eve.http.hostname', + type: 'alias', + }, + 'suricata.eve.http.length': { + category: 'suricata', + name: 'suricata.eve.http.length', + type: 'alias', + }, + 'suricata.eve.http.http_method': { + category: 'suricata', + name: 'suricata.eve.http.http_method', + type: 'alias', + }, + 'suricata.eve.http.http_content_type': { + category: 'suricata', + name: 'suricata.eve.http.http_content_type', + type: 'keyword', + }, + 'suricata.eve.timestamp': { + category: 'suricata', + name: 'suricata.eve.timestamp', + type: 'alias', + }, + 'suricata.eve.in_iface': { + category: 'suricata', + name: 'suricata.eve.in_iface', + type: 'keyword', + }, + 'suricata.eve.alert.category': { + category: 'suricata', + name: 'suricata.eve.alert.category', + type: 'keyword', + }, + 'suricata.eve.alert.severity': { + category: 'suricata', + name: 'suricata.eve.alert.severity', + type: 'alias', + }, + 'suricata.eve.alert.rev': { + category: 'suricata', + name: 'suricata.eve.alert.rev', + type: 'long', + }, + 'suricata.eve.alert.gid': { + category: 'suricata', + name: 'suricata.eve.alert.gid', + type: 'long', + }, + 'suricata.eve.alert.signature': { + category: 'suricata', + name: 'suricata.eve.alert.signature', + type: 'keyword', + }, + 'suricata.eve.alert.action': { + category: 'suricata', + name: 'suricata.eve.alert.action', + type: 'alias', + }, + 'suricata.eve.alert.signature_id': { + category: 'suricata', + name: 'suricata.eve.alert.signature_id', + type: 'long', + }, + 'suricata.eve.ssh.client.proto_version': { + category: 'suricata', + name: 'suricata.eve.ssh.client.proto_version', + type: 'keyword', + }, + 'suricata.eve.ssh.client.software_version': { + category: 'suricata', + name: 'suricata.eve.ssh.client.software_version', + type: 'keyword', + }, + 'suricata.eve.ssh.server.proto_version': { + category: 'suricata', + name: 'suricata.eve.ssh.server.proto_version', + type: 'keyword', + }, + 'suricata.eve.ssh.server.software_version': { + category: 'suricata', + name: 'suricata.eve.ssh.server.software_version', + type: 'keyword', + }, + 'suricata.eve.stats.capture.kernel_packets': { + category: 'suricata', + name: 'suricata.eve.stats.capture.kernel_packets', + type: 'long', + }, + 'suricata.eve.stats.capture.kernel_drops': { + category: 'suricata', + name: 'suricata.eve.stats.capture.kernel_drops', + type: 'long', + }, + 'suricata.eve.stats.capture.kernel_ifdrops': { + category: 'suricata', + name: 'suricata.eve.stats.capture.kernel_ifdrops', + type: 'long', + }, + 'suricata.eve.stats.uptime': { + category: 'suricata', + name: 'suricata.eve.stats.uptime', + type: 'long', + }, + 'suricata.eve.stats.detect.alert': { + category: 'suricata', + name: 'suricata.eve.stats.detect.alert', + type: 'long', + }, + 'suricata.eve.stats.http.memcap': { + category: 'suricata', + name: 'suricata.eve.stats.http.memcap', + type: 'long', + }, + 'suricata.eve.stats.http.memuse': { + category: 'suricata', + name: 'suricata.eve.stats.http.memuse', + type: 'long', + }, + 'suricata.eve.stats.file_store.open_files': { + category: 'suricata', + name: 'suricata.eve.stats.file_store.open_files', + type: 'long', + }, + 'suricata.eve.stats.defrag.max_frag_hits': { + category: 'suricata', + name: 'suricata.eve.stats.defrag.max_frag_hits', + type: 'long', + }, + 'suricata.eve.stats.defrag.ipv4.timeouts': { + category: 'suricata', + name: 'suricata.eve.stats.defrag.ipv4.timeouts', + type: 'long', + }, + 'suricata.eve.stats.defrag.ipv4.fragments': { + category: 'suricata', + name: 'suricata.eve.stats.defrag.ipv4.fragments', + type: 'long', + }, + 'suricata.eve.stats.defrag.ipv4.reassembled': { + category: 'suricata', + name: 'suricata.eve.stats.defrag.ipv4.reassembled', + type: 'long', + }, + 'suricata.eve.stats.defrag.ipv6.timeouts': { + category: 'suricata', + name: 'suricata.eve.stats.defrag.ipv6.timeouts', + type: 'long', + }, + 'suricata.eve.stats.defrag.ipv6.fragments': { + category: 'suricata', + name: 'suricata.eve.stats.defrag.ipv6.fragments', + type: 'long', + }, + 'suricata.eve.stats.defrag.ipv6.reassembled': { + category: 'suricata', + name: 'suricata.eve.stats.defrag.ipv6.reassembled', + type: 'long', + }, + 'suricata.eve.stats.flow.tcp_reuse': { + category: 'suricata', + name: 'suricata.eve.stats.flow.tcp_reuse', + type: 'long', + }, + 'suricata.eve.stats.flow.udp': { + category: 'suricata', + name: 'suricata.eve.stats.flow.udp', + type: 'long', + }, + 'suricata.eve.stats.flow.memcap': { + category: 'suricata', + name: 'suricata.eve.stats.flow.memcap', + type: 'long', + }, + 'suricata.eve.stats.flow.emerg_mode_entered': { + category: 'suricata', + name: 'suricata.eve.stats.flow.emerg_mode_entered', + type: 'long', + }, + 'suricata.eve.stats.flow.emerg_mode_over': { + category: 'suricata', + name: 'suricata.eve.stats.flow.emerg_mode_over', + type: 'long', + }, + 'suricata.eve.stats.flow.tcp': { + category: 'suricata', + name: 'suricata.eve.stats.flow.tcp', + type: 'long', + }, + 'suricata.eve.stats.flow.icmpv6': { + category: 'suricata', + name: 'suricata.eve.stats.flow.icmpv6', + type: 'long', + }, + 'suricata.eve.stats.flow.icmpv4': { + category: 'suricata', + name: 'suricata.eve.stats.flow.icmpv4', + type: 'long', + }, + 'suricata.eve.stats.flow.spare': { + category: 'suricata', + name: 'suricata.eve.stats.flow.spare', + type: 'long', + }, + 'suricata.eve.stats.flow.memuse': { + category: 'suricata', + name: 'suricata.eve.stats.flow.memuse', + type: 'long', + }, + 'suricata.eve.stats.tcp.pseudo_failed': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.pseudo_failed', + type: 'long', + }, + 'suricata.eve.stats.tcp.ssn_memcap_drop': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.ssn_memcap_drop', + type: 'long', + }, + 'suricata.eve.stats.tcp.insert_data_overlap_fail': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.insert_data_overlap_fail', + type: 'long', + }, + 'suricata.eve.stats.tcp.sessions': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.sessions', + type: 'long', + }, + 'suricata.eve.stats.tcp.pseudo': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.pseudo', + type: 'long', + }, + 'suricata.eve.stats.tcp.synack': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.synack', + type: 'long', + }, + 'suricata.eve.stats.tcp.insert_data_normal_fail': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.insert_data_normal_fail', + type: 'long', + }, + 'suricata.eve.stats.tcp.syn': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.syn', + type: 'long', + }, + 'suricata.eve.stats.tcp.memuse': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.memuse', + type: 'long', + }, + 'suricata.eve.stats.tcp.invalid_checksum': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.invalid_checksum', + type: 'long', + }, + 'suricata.eve.stats.tcp.segment_memcap_drop': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.segment_memcap_drop', + type: 'long', + }, + 'suricata.eve.stats.tcp.overlap': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.overlap', + type: 'long', + }, + 'suricata.eve.stats.tcp.insert_list_fail': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.insert_list_fail', + type: 'long', + }, + 'suricata.eve.stats.tcp.rst': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.rst', + type: 'long', + }, + 'suricata.eve.stats.tcp.stream_depth_reached': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.stream_depth_reached', + type: 'long', + }, + 'suricata.eve.stats.tcp.reassembly_memuse': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.reassembly_memuse', + type: 'long', + }, + 'suricata.eve.stats.tcp.reassembly_gap': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.reassembly_gap', + type: 'long', + }, + 'suricata.eve.stats.tcp.overlap_diff_data': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.overlap_diff_data', + type: 'long', + }, + 'suricata.eve.stats.tcp.no_flow': { + category: 'suricata', + name: 'suricata.eve.stats.tcp.no_flow', + type: 'long', + }, + 'suricata.eve.stats.decoder.avg_pkt_size': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.avg_pkt_size', + type: 'long', + }, + 'suricata.eve.stats.decoder.bytes': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.bytes', + type: 'long', + }, + 'suricata.eve.stats.decoder.tcp': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.tcp', + type: 'long', + }, + 'suricata.eve.stats.decoder.raw': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.raw', + type: 'long', + }, + 'suricata.eve.stats.decoder.ppp': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ppp', + type: 'long', + }, + 'suricata.eve.stats.decoder.vlan_qinq': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.vlan_qinq', + type: 'long', + }, + 'suricata.eve.stats.decoder.null': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.null', + type: 'long', + }, + 'suricata.eve.stats.decoder.ltnull.unsupported_type': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ltnull.unsupported_type', + type: 'long', + }, + 'suricata.eve.stats.decoder.ltnull.pkt_too_small': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ltnull.pkt_too_small', + type: 'long', + }, + 'suricata.eve.stats.decoder.invalid': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.invalid', + type: 'long', + }, + 'suricata.eve.stats.decoder.gre': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.gre', + type: 'long', + }, + 'suricata.eve.stats.decoder.ipv4': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ipv4', + type: 'long', + }, + 'suricata.eve.stats.decoder.ipv6': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ipv6', + type: 'long', + }, + 'suricata.eve.stats.decoder.pkts': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.pkts', + type: 'long', + }, + 'suricata.eve.stats.decoder.ipv6_in_ipv6': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ipv6_in_ipv6', + type: 'long', + }, + 'suricata.eve.stats.decoder.ipraw.invalid_ip_version': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ipraw.invalid_ip_version', + type: 'long', + }, + 'suricata.eve.stats.decoder.pppoe': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.pppoe', + type: 'long', + }, + 'suricata.eve.stats.decoder.udp': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.udp', + type: 'long', + }, + 'suricata.eve.stats.decoder.dce.pkt_too_small': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.dce.pkt_too_small', + type: 'long', + }, + 'suricata.eve.stats.decoder.vlan': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.vlan', + type: 'long', + }, + 'suricata.eve.stats.decoder.sctp': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.sctp', + type: 'long', + }, + 'suricata.eve.stats.decoder.max_pkt_size': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.max_pkt_size', + type: 'long', + }, + 'suricata.eve.stats.decoder.teredo': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.teredo', + type: 'long', + }, + 'suricata.eve.stats.decoder.mpls': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.mpls', + type: 'long', + }, + 'suricata.eve.stats.decoder.sll': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.sll', + type: 'long', + }, + 'suricata.eve.stats.decoder.icmpv6': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.icmpv6', + type: 'long', + }, + 'suricata.eve.stats.decoder.icmpv4': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.icmpv4', + type: 'long', + }, + 'suricata.eve.stats.decoder.erspan': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.erspan', + type: 'long', + }, + 'suricata.eve.stats.decoder.ethernet': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ethernet', + type: 'long', + }, + 'suricata.eve.stats.decoder.ipv4_in_ipv6': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ipv4_in_ipv6', + type: 'long', + }, + 'suricata.eve.stats.decoder.ieee8021ah': { + category: 'suricata', + name: 'suricata.eve.stats.decoder.ieee8021ah', + type: 'long', + }, + 'suricata.eve.stats.dns.memcap_global': { + category: 'suricata', + name: 'suricata.eve.stats.dns.memcap_global', + type: 'long', + }, + 'suricata.eve.stats.dns.memcap_state': { + category: 'suricata', + name: 'suricata.eve.stats.dns.memcap_state', + type: 'long', + }, + 'suricata.eve.stats.dns.memuse': { + category: 'suricata', + name: 'suricata.eve.stats.dns.memuse', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.rows_busy': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.rows_busy', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.flows_timeout': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.flows_timeout', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.flows_notimeout': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.flows_notimeout', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.rows_skipped': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.rows_skipped', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.closed_pruned': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.closed_pruned', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.new_pruned': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.new_pruned', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.flows_removed': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.flows_removed', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.bypassed_pruned': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.bypassed_pruned', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.est_pruned': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.est_pruned', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.flows_timeout_inuse': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.flows_timeout_inuse', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.flows_checked': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.flows_checked', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.rows_maxlen': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.rows_maxlen', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.rows_checked': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.rows_checked', + type: 'long', + }, + 'suricata.eve.stats.flow_mgr.rows_empty': { + category: 'suricata', + name: 'suricata.eve.stats.flow_mgr.rows_empty', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.tls': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.tls', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.ftp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.ftp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.http': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.http', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.failed_udp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.failed_udp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.dns_udp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.dns_udp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.dns_tcp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.dns_tcp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.smtp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.smtp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.failed_tcp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.failed_tcp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.msn': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.msn', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.ssh': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.ssh', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.imap': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.imap', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.dcerpc_udp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.dcerpc_udp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.dcerpc_tcp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.dcerpc_tcp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.flow.smb': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.flow.smb', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.tls': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.tls', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.ftp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.ftp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.http': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.http', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.dns_udp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.dns_udp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.dns_tcp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.dns_tcp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.smtp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.smtp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.ssh': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.ssh', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.dcerpc_udp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.dcerpc_udp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.dcerpc_tcp': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.dcerpc_tcp', + type: 'long', + }, + 'suricata.eve.stats.app_layer.tx.smb': { + category: 'suricata', + name: 'suricata.eve.stats.app_layer.tx.smb', + type: 'long', + }, + 'suricata.eve.tls.notbefore': { + category: 'suricata', + name: 'suricata.eve.tls.notbefore', + type: 'date', + }, + 'suricata.eve.tls.issuerdn': { + category: 'suricata', + name: 'suricata.eve.tls.issuerdn', + type: 'keyword', + }, + 'suricata.eve.tls.sni': { + category: 'suricata', + name: 'suricata.eve.tls.sni', + type: 'keyword', + }, + 'suricata.eve.tls.version': { + category: 'suricata', + name: 'suricata.eve.tls.version', + type: 'keyword', + }, + 'suricata.eve.tls.session_resumed': { + category: 'suricata', + name: 'suricata.eve.tls.session_resumed', + type: 'boolean', + }, + 'suricata.eve.tls.fingerprint': { + category: 'suricata', + name: 'suricata.eve.tls.fingerprint', + type: 'keyword', + }, + 'suricata.eve.tls.serial': { + category: 'suricata', + name: 'suricata.eve.tls.serial', + type: 'keyword', + }, + 'suricata.eve.tls.notafter': { + category: 'suricata', + name: 'suricata.eve.tls.notafter', + type: 'date', + }, + 'suricata.eve.tls.subject': { + category: 'suricata', + name: 'suricata.eve.tls.subject', + type: 'keyword', + }, + 'suricata.eve.tls.ja3s.string': { + category: 'suricata', + name: 'suricata.eve.tls.ja3s.string', + type: 'keyword', + }, + 'suricata.eve.tls.ja3s.hash': { + category: 'suricata', + name: 'suricata.eve.tls.ja3s.hash', + type: 'keyword', + }, + 'suricata.eve.tls.ja3.string': { + category: 'suricata', + name: 'suricata.eve.tls.ja3.string', + type: 'keyword', + }, + 'suricata.eve.tls.ja3.hash': { + category: 'suricata', + name: 'suricata.eve.tls.ja3.hash', + type: 'keyword', + }, + 'suricata.eve.app_proto_ts': { + category: 'suricata', + name: 'suricata.eve.app_proto_ts', + type: 'keyword', + }, + 'suricata.eve.flow.bytes_toclient': { + category: 'suricata', + name: 'suricata.eve.flow.bytes_toclient', + type: 'alias', + }, + 'suricata.eve.flow.start': { + category: 'suricata', + name: 'suricata.eve.flow.start', + type: 'alias', + }, + 'suricata.eve.flow.pkts_toclient': { + category: 'suricata', + name: 'suricata.eve.flow.pkts_toclient', + type: 'alias', + }, + 'suricata.eve.flow.age': { + category: 'suricata', + name: 'suricata.eve.flow.age', + type: 'long', + }, + 'suricata.eve.flow.state': { + category: 'suricata', + name: 'suricata.eve.flow.state', + type: 'keyword', + }, + 'suricata.eve.flow.bytes_toserver': { + category: 'suricata', + name: 'suricata.eve.flow.bytes_toserver', + type: 'alias', + }, + 'suricata.eve.flow.reason': { + category: 'suricata', + name: 'suricata.eve.flow.reason', + type: 'keyword', + }, + 'suricata.eve.flow.pkts_toserver': { + category: 'suricata', + name: 'suricata.eve.flow.pkts_toserver', + type: 'alias', + }, + 'suricata.eve.flow.end': { + category: 'suricata', + name: 'suricata.eve.flow.end', + type: 'date', + }, + 'suricata.eve.flow.alerted': { + category: 'suricata', + name: 'suricata.eve.flow.alerted', + type: 'boolean', + }, + 'suricata.eve.app_proto': { + category: 'suricata', + name: 'suricata.eve.app_proto', + type: 'alias', + }, + 'suricata.eve.tx_id': { + category: 'suricata', + name: 'suricata.eve.tx_id', + type: 'long', + }, + 'suricata.eve.app_proto_tc': { + category: 'suricata', + name: 'suricata.eve.app_proto_tc', + type: 'keyword', + }, + 'suricata.eve.smtp.rcpt_to': { + category: 'suricata', + name: 'suricata.eve.smtp.rcpt_to', + type: 'keyword', + }, + 'suricata.eve.smtp.mail_from': { + category: 'suricata', + name: 'suricata.eve.smtp.mail_from', + type: 'keyword', + }, + 'suricata.eve.smtp.helo': { + category: 'suricata', + name: 'suricata.eve.smtp.helo', + type: 'keyword', + }, + 'suricata.eve.app_proto_expected': { + category: 'suricata', + name: 'suricata.eve.app_proto_expected', + type: 'keyword', + }, + 'suricata.eve.flags': { + category: 'suricata', + name: 'suricata.eve.flags', + type: 'group', + }, + 'zeek.session_id': { + category: 'zeek', + description: 'A unique identifier of the session ', + name: 'zeek.session_id', + type: 'keyword', + }, + 'zeek.capture_loss.ts_delta': { + category: 'zeek', + description: 'The time delay between this measurement and the last. ', + name: 'zeek.capture_loss.ts_delta', + type: 'integer', + }, + 'zeek.capture_loss.peer': { + category: 'zeek', + description: + 'In the event that there are multiple Bro instances logging to the same host, this distinguishes each peer with its individual name. ', + name: 'zeek.capture_loss.peer', + type: 'keyword', + }, + 'zeek.capture_loss.gaps': { + category: 'zeek', + description: 'Number of missed ACKs from the previous measurement interval. ', + name: 'zeek.capture_loss.gaps', + type: 'integer', + }, + 'zeek.capture_loss.acks': { + category: 'zeek', + description: 'Total number of ACKs seen in the previous measurement interval. ', + name: 'zeek.capture_loss.acks', + type: 'integer', + }, + 'zeek.capture_loss.percent_lost': { + category: 'zeek', + description: "Percentage of ACKs seen where the data being ACKed wasn't seen. ", + name: 'zeek.capture_loss.percent_lost', + type: 'double', + }, + 'zeek.connection.local_orig': { + category: 'zeek', + description: 'Indicates whether the session is originated locally. ', + name: 'zeek.connection.local_orig', + type: 'boolean', + }, + 'zeek.connection.local_resp': { + category: 'zeek', + description: 'Indicates whether the session is responded locally. ', + name: 'zeek.connection.local_resp', + type: 'boolean', + }, + 'zeek.connection.missed_bytes': { + category: 'zeek', + description: 'Missed bytes for the session. ', + name: 'zeek.connection.missed_bytes', + type: 'long', + }, + 'zeek.connection.state': { + category: 'zeek', + description: 'Code indicating the state of the session. ', + name: 'zeek.connection.state', + type: 'keyword', + }, + 'zeek.connection.state_message': { + category: 'zeek', + description: 'The state of the session. ', + name: 'zeek.connection.state_message', + type: 'keyword', + }, + 'zeek.connection.icmp.type': { + category: 'zeek', + description: 'ICMP message type. ', + name: 'zeek.connection.icmp.type', + type: 'integer', + }, + 'zeek.connection.icmp.code': { + category: 'zeek', + description: 'ICMP message code. ', + name: 'zeek.connection.icmp.code', + type: 'integer', + }, + 'zeek.connection.history': { + category: 'zeek', + description: 'Flags indicating the history of the session. ', + name: 'zeek.connection.history', + type: 'keyword', + }, + 'zeek.connection.vlan': { + category: 'zeek', + description: 'VLAN identifier. ', + name: 'zeek.connection.vlan', + type: 'integer', + }, + 'zeek.connection.inner_vlan': { + category: 'zeek', + description: 'VLAN identifier. ', + name: 'zeek.connection.inner_vlan', + type: 'integer', + }, + 'zeek.dce_rpc.rtt': { + category: 'zeek', + description: + "Round trip time from the request to the response. If either the request or response wasn't seen, this will be null. ", + name: 'zeek.dce_rpc.rtt', + type: 'integer', + }, + 'zeek.dce_rpc.named_pipe': { + category: 'zeek', + description: 'Remote pipe name. ', + name: 'zeek.dce_rpc.named_pipe', + type: 'keyword', + }, + 'zeek.dce_rpc.endpoint': { + category: 'zeek', + description: 'Endpoint name looked up from the uuid. ', + name: 'zeek.dce_rpc.endpoint', + type: 'keyword', + }, + 'zeek.dce_rpc.operation': { + category: 'zeek', + description: 'Operation seen in the call. ', + name: 'zeek.dce_rpc.operation', + type: 'keyword', + }, + 'zeek.dhcp.domain': { + category: 'zeek', + description: 'Domain given by the server in option 15. ', + name: 'zeek.dhcp.domain', + type: 'keyword', + }, + 'zeek.dhcp.duration': { + category: 'zeek', + description: + 'Duration of the DHCP session representing the time from the first message to the last, in seconds. ', + name: 'zeek.dhcp.duration', + type: 'double', + }, + 'zeek.dhcp.hostname': { + category: 'zeek', + description: 'Name given by client in Hostname option 12. ', + name: 'zeek.dhcp.hostname', + type: 'keyword', + }, + 'zeek.dhcp.client_fqdn': { + category: 'zeek', + description: 'FQDN given by client in Client FQDN option 81. ', + name: 'zeek.dhcp.client_fqdn', + type: 'keyword', + }, + 'zeek.dhcp.lease_time': { + category: 'zeek', + description: 'IP address lease interval in seconds. ', + name: 'zeek.dhcp.lease_time', + type: 'integer', + }, + 'zeek.dhcp.address.assigned': { + category: 'zeek', + description: 'IP address assigned by the server. ', + name: 'zeek.dhcp.address.assigned', + type: 'ip', + }, + 'zeek.dhcp.address.client': { + category: 'zeek', + description: + 'IP address of the client. If a transaction is only a client sending INFORM messages then there is no lease information exchanged so this is helpful to know who sent the messages. Getting an address in this field does require that the client sources at least one DHCP message using a non-broadcast address. ', + name: 'zeek.dhcp.address.client', + type: 'ip', + }, + 'zeek.dhcp.address.mac': { + category: 'zeek', + description: "Client's hardware address. ", + name: 'zeek.dhcp.address.mac', + type: 'keyword', + }, + 'zeek.dhcp.address.requested': { + category: 'zeek', + description: 'IP address requested by the client. ', + name: 'zeek.dhcp.address.requested', + type: 'ip', + }, + 'zeek.dhcp.address.server': { + category: 'zeek', + description: 'IP address of the DHCP server. ', + name: 'zeek.dhcp.address.server', + type: 'ip', + }, + 'zeek.dhcp.msg.types': { + category: 'zeek', + description: 'List of DHCP message types seen in this exchange. ', + name: 'zeek.dhcp.msg.types', + type: 'keyword', + }, + 'zeek.dhcp.msg.origin': { + category: 'zeek', + description: + '(present if policy/protocols/dhcp/msg-orig.bro is loaded) The address that originated each message from the msg.types field. ', + name: 'zeek.dhcp.msg.origin', + type: 'ip', + }, + 'zeek.dhcp.msg.client': { + category: 'zeek', + description: + 'Message typically accompanied with a DHCP_DECLINE so the client can tell the server why it rejected an address. ', + name: 'zeek.dhcp.msg.client', + type: 'keyword', + }, + 'zeek.dhcp.msg.server': { + category: 'zeek', + description: + 'Message typically accompanied with a DHCP_NAK to let the client know why it rejected the request. ', + name: 'zeek.dhcp.msg.server', + type: 'keyword', + }, + 'zeek.dhcp.software.client': { + category: 'zeek', + description: + '(present if policy/protocols/dhcp/software.bro is loaded) Software reported by the client in the vendor_class option. ', + name: 'zeek.dhcp.software.client', + type: 'keyword', + }, + 'zeek.dhcp.software.server': { + category: 'zeek', + description: + '(present if policy/protocols/dhcp/software.bro is loaded) Software reported by the client in the vendor_class option. ', + name: 'zeek.dhcp.software.server', + type: 'keyword', + }, + 'zeek.dhcp.id.circuit': { + category: 'zeek', + description: + '(present if policy/protocols/dhcp/sub-opts.bro is loaded) Added by DHCP relay agents which terminate switched or permanent circuits. It encodes an agent-local identifier of the circuit from which a DHCP client-to-server packet was received. Typically it should represent a router or switch interface number. ', + name: 'zeek.dhcp.id.circuit', + type: 'keyword', + }, + 'zeek.dhcp.id.remote_agent': { + category: 'zeek', + description: + '(present if policy/protocols/dhcp/sub-opts.bro is loaded) A globally unique identifier added by relay agents to identify the remote host end of the circuit. ', + name: 'zeek.dhcp.id.remote_agent', + type: 'keyword', + }, + 'zeek.dhcp.id.subscriber': { + category: 'zeek', + description: + "(present if policy/protocols/dhcp/sub-opts.bro is loaded) The subscriber ID is a value independent of the physical network configuration so that a customer's DHCP configuration can be given to them correctly no matter where they are physically connected. ", + name: 'zeek.dhcp.id.subscriber', + type: 'keyword', + }, + 'zeek.dnp3.function.request': { + category: 'zeek', + description: 'The name of the function message in the request. ', + name: 'zeek.dnp3.function.request', + type: 'keyword', + }, + 'zeek.dnp3.function.reply': { + category: 'zeek', + description: 'The name of the function message in the reply. ', + name: 'zeek.dnp3.function.reply', + type: 'keyword', + }, + 'zeek.dnp3.id': { + category: 'zeek', + description: "The response's internal indication number. ", + name: 'zeek.dnp3.id', + type: 'integer', + }, + 'zeek.dns.trans_id': { + category: 'zeek', + description: 'DNS transaction identifier. ', + name: 'zeek.dns.trans_id', + type: 'keyword', + }, + 'zeek.dns.rtt': { + category: 'zeek', + description: 'Round trip time for the query and response. ', + name: 'zeek.dns.rtt', + type: 'double', + }, + 'zeek.dns.query': { + category: 'zeek', + description: 'The domain name that is the subject of the DNS query. ', + name: 'zeek.dns.query', + type: 'keyword', + }, + 'zeek.dns.qclass': { + category: 'zeek', + description: 'The QCLASS value specifying the class of the query. ', + name: 'zeek.dns.qclass', + type: 'long', + }, + 'zeek.dns.qclass_name': { + category: 'zeek', + description: 'A descriptive name for the class of the query. ', + name: 'zeek.dns.qclass_name', + type: 'keyword', + }, + 'zeek.dns.qtype': { + category: 'zeek', + description: 'A QTYPE value specifying the type of the query. ', + name: 'zeek.dns.qtype', + type: 'long', + }, + 'zeek.dns.qtype_name': { + category: 'zeek', + description: 'A descriptive name for the type of the query. ', + name: 'zeek.dns.qtype_name', + type: 'keyword', + }, + 'zeek.dns.rcode': { + category: 'zeek', + description: 'The response code value in DNS response messages. ', + name: 'zeek.dns.rcode', + type: 'long', + }, + 'zeek.dns.rcode_name': { + category: 'zeek', + description: 'A descriptive name for the response code value. ', + name: 'zeek.dns.rcode_name', + type: 'keyword', + }, + 'zeek.dns.AA': { + category: 'zeek', + description: + 'The Authoritative Answer bit for response messages specifies that the responding name server is an authority for the domain name in the question section. ', + name: 'zeek.dns.AA', + type: 'boolean', + }, + 'zeek.dns.TC': { + category: 'zeek', + description: 'The Truncation bit specifies that the message was truncated. ', + name: 'zeek.dns.TC', + type: 'boolean', + }, + 'zeek.dns.RD': { + category: 'zeek', + description: + 'The Recursion Desired bit in a request message indicates that the client wants recursive service for this query. ', + name: 'zeek.dns.RD', + type: 'boolean', + }, + 'zeek.dns.RA': { + category: 'zeek', + description: + 'The Recursion Available bit in a response message indicates that the name server supports recursive queries. ', + name: 'zeek.dns.RA', + type: 'boolean', + }, + 'zeek.dns.answers': { + category: 'zeek', + description: 'The set of resource descriptions in the query answer. ', + name: 'zeek.dns.answers', + type: 'keyword', + }, + 'zeek.dns.TTLs': { + category: 'zeek', + description: 'The caching intervals of the associated RRs described by the answers field. ', + name: 'zeek.dns.TTLs', + type: 'double', + }, + 'zeek.dns.rejected': { + category: 'zeek', + description: 'Indicates whether the DNS query was rejected by the server. ', + name: 'zeek.dns.rejected', + type: 'boolean', + }, + 'zeek.dns.total_answers': { + category: 'zeek', + description: 'The total number of resource records in the reply. ', + name: 'zeek.dns.total_answers', + type: 'integer', + }, + 'zeek.dns.total_replies': { + category: 'zeek', + description: 'The total number of resource records in the reply message. ', + name: 'zeek.dns.total_replies', + type: 'integer', + }, + 'zeek.dns.saw_query': { + category: 'zeek', + description: 'Whether the full DNS query has been seen. ', + name: 'zeek.dns.saw_query', + type: 'boolean', + }, + 'zeek.dns.saw_reply': { + category: 'zeek', + description: 'Whether the full DNS reply has been seen. ', + name: 'zeek.dns.saw_reply', + type: 'boolean', + }, + 'zeek.dpd.analyzer': { + category: 'zeek', + description: 'The analyzer that generated the violation. ', + name: 'zeek.dpd.analyzer', + type: 'keyword', + }, + 'zeek.dpd.failure_reason': { + category: 'zeek', + description: 'The textual reason for the analysis failure. ', + name: 'zeek.dpd.failure_reason', + type: 'keyword', + }, + 'zeek.dpd.packet_segment': { + category: 'zeek', + description: + '(present if policy/frameworks/dpd/packet-segment-logging.bro is loaded) A chunk of the payload that most likely resulted in the protocol violation. ', + name: 'zeek.dpd.packet_segment', + type: 'keyword', + }, + 'zeek.files.fuid': { + category: 'zeek', + description: 'A file unique identifier. ', + name: 'zeek.files.fuid', + type: 'keyword', + }, + 'zeek.files.tx_host': { + category: 'zeek', + description: 'The host that transferred the file. ', + name: 'zeek.files.tx_host', + type: 'ip', + }, + 'zeek.files.rx_host': { + category: 'zeek', + description: 'The host that received the file. ', + name: 'zeek.files.rx_host', + type: 'ip', + }, + 'zeek.files.session_ids': { + category: 'zeek', + description: 'The sessions that have this file. ', + name: 'zeek.files.session_ids', + type: 'keyword', + }, + 'zeek.files.source': { + category: 'zeek', + description: + 'An identification of the source of the file data. E.g. it may be a network protocol over which it was transferred, or a local file path which was read, or some other input source. ', + name: 'zeek.files.source', + type: 'keyword', + }, + 'zeek.files.depth': { + category: 'zeek', + description: + 'A value to represent the depth of this file in relation to its source. In SMTP, it is the depth of the MIME attachment on the message. In HTTP, it is the depth of the request within the TCP connection. ', + name: 'zeek.files.depth', + type: 'long', + }, + 'zeek.files.analyzers': { + category: 'zeek', + description: 'A set of analysis types done during the file analysis. ', + name: 'zeek.files.analyzers', + type: 'keyword', + }, + 'zeek.files.mime_type': { + category: 'zeek', + description: 'Mime type of the file. ', + name: 'zeek.files.mime_type', + type: 'keyword', + }, + 'zeek.files.filename': { + category: 'zeek', + description: 'Name of the file if available. ', + name: 'zeek.files.filename', + type: 'keyword', + }, + 'zeek.files.local_orig': { + category: 'zeek', + description: + 'If the source of this file is a network connection, this field indicates if the data originated from the local network or not. ', + name: 'zeek.files.local_orig', + type: 'boolean', + }, + 'zeek.files.is_orig': { + category: 'zeek', + description: + 'If the source of this file is a network connection, this field indicates if the file is being sent by the originator of the connection or the responder. ', + name: 'zeek.files.is_orig', + type: 'boolean', + }, + 'zeek.files.duration': { + category: 'zeek', + description: 'The duration the file was analyzed for. Not the duration of the session. ', + name: 'zeek.files.duration', + type: 'double', + }, + 'zeek.files.seen_bytes': { + category: 'zeek', + description: 'Number of bytes provided to the file analysis engine for the file. ', + name: 'zeek.files.seen_bytes', + type: 'long', + }, + 'zeek.files.total_bytes': { + category: 'zeek', + description: 'Total number of bytes that are supposed to comprise the full file. ', + name: 'zeek.files.total_bytes', + type: 'long', + }, + 'zeek.files.missing_bytes': { + category: 'zeek', + description: + 'The number of bytes in the file stream that were completely missed during the process of analysis. ', + name: 'zeek.files.missing_bytes', + type: 'long', + }, + 'zeek.files.overflow_bytes': { + category: 'zeek', + description: + "The number of bytes in the file stream that were not delivered to stream file analyzers. This could be overlapping bytes or bytes that couldn't be reassembled. ", + name: 'zeek.files.overflow_bytes', + type: 'long', + }, + 'zeek.files.timedout': { + category: 'zeek', + description: 'Whether the file analysis timed out at least once for the file. ', + name: 'zeek.files.timedout', + type: 'boolean', + }, + 'zeek.files.parent_fuid': { + category: 'zeek', + description: + 'Identifier associated with a container file from which this one was extracted as part of the file analysis. ', + name: 'zeek.files.parent_fuid', + type: 'keyword', + }, + 'zeek.files.md5': { + category: 'zeek', + description: 'An MD5 digest of the file contents. ', + name: 'zeek.files.md5', + type: 'keyword', + }, + 'zeek.files.sha1': { + category: 'zeek', + description: 'A SHA1 digest of the file contents. ', + name: 'zeek.files.sha1', + type: 'keyword', + }, + 'zeek.files.sha256': { + category: 'zeek', + description: 'A SHA256 digest of the file contents. ', + name: 'zeek.files.sha256', + type: 'keyword', + }, + 'zeek.files.extracted': { + category: 'zeek', + description: 'Local filename of extracted file. ', + name: 'zeek.files.extracted', + type: 'keyword', + }, + 'zeek.files.extracted_cutoff': { + category: 'zeek', + description: + 'Indicate whether the file being extracted was cut off hence not extracted completely. ', + name: 'zeek.files.extracted_cutoff', + type: 'boolean', + }, + 'zeek.files.extracted_size': { + category: 'zeek', + description: 'The number of bytes extracted to disk. ', + name: 'zeek.files.extracted_size', + type: 'long', + }, + 'zeek.files.entropy': { + category: 'zeek', + description: 'The information density of the contents of the file. ', + name: 'zeek.files.entropy', + type: 'double', + }, + 'zeek.ftp.user': { + category: 'zeek', + description: 'User name for the current FTP session. ', + name: 'zeek.ftp.user', + type: 'keyword', + }, + 'zeek.ftp.password': { + category: 'zeek', + description: 'Password for the current FTP session if captured. ', + name: 'zeek.ftp.password', + type: 'keyword', + }, + 'zeek.ftp.command': { + category: 'zeek', + description: 'Command given by the client. ', + name: 'zeek.ftp.command', + type: 'keyword', + }, + 'zeek.ftp.arg': { + category: 'zeek', + description: 'Argument for the command if one is given. ', + name: 'zeek.ftp.arg', + type: 'keyword', + }, + 'zeek.ftp.file.size': { + category: 'zeek', + description: 'Size of the file if the command indicates a file transfer. ', + name: 'zeek.ftp.file.size', + type: 'long', + }, + 'zeek.ftp.file.mime_type': { + category: 'zeek', + description: 'Sniffed mime type of file. ', + name: 'zeek.ftp.file.mime_type', + type: 'keyword', + }, + 'zeek.ftp.file.fuid': { + category: 'zeek', + description: '(present if base/protocols/ftp/files.bro is loaded) File unique ID. ', + name: 'zeek.ftp.file.fuid', + type: 'keyword', + }, + 'zeek.ftp.reply.code': { + category: 'zeek', + description: 'Reply code from the server in response to the command. ', + name: 'zeek.ftp.reply.code', + type: 'integer', + }, + 'zeek.ftp.reply.msg': { + category: 'zeek', + description: 'Reply message from the server in response to the command. ', + name: 'zeek.ftp.reply.msg', + type: 'keyword', + }, + 'zeek.ftp.data_channel.passive': { + category: 'zeek', + description: 'Whether PASV mode is toggled for control channel. ', + name: 'zeek.ftp.data_channel.passive', + type: 'boolean', + }, + 'zeek.ftp.data_channel.originating_host': { + category: 'zeek', + description: 'The host that will be initiating the data connection. ', + name: 'zeek.ftp.data_channel.originating_host', + type: 'ip', + }, + 'zeek.ftp.data_channel.response_host': { + category: 'zeek', + description: 'The host that will be accepting the data connection. ', + name: 'zeek.ftp.data_channel.response_host', + type: 'ip', + }, + 'zeek.ftp.data_channel.response_port': { + category: 'zeek', + description: 'The port at which the acceptor is listening for the data connection. ', + name: 'zeek.ftp.data_channel.response_port', + type: 'integer', + }, + 'zeek.ftp.cwd': { + category: 'zeek', + description: + "Current working directory that this session is in. By making the default value '.', we can indicate that unless something more concrete is discovered that the existing but unknown directory is ok to use. ", + name: 'zeek.ftp.cwd', + type: 'keyword', + }, + 'zeek.ftp.cmdarg.cmd': { + category: 'zeek', + description: 'Command. ', + name: 'zeek.ftp.cmdarg.cmd', + type: 'keyword', + }, + 'zeek.ftp.cmdarg.arg': { + category: 'zeek', + description: 'Argument for the command if one was given. ', + name: 'zeek.ftp.cmdarg.arg', + type: 'keyword', + }, + 'zeek.ftp.cmdarg.seq': { + category: 'zeek', + description: 'Counter to track how many commands have been executed. ', + name: 'zeek.ftp.cmdarg.seq', + type: 'integer', + }, + 'zeek.ftp.pending_commands': { + category: 'zeek', + description: + 'Queue for commands that have been sent but not yet responded to are tracked here. ', + name: 'zeek.ftp.pending_commands', + type: 'integer', + }, + 'zeek.ftp.passive': { + category: 'zeek', + description: 'Indicates if the session is in active or passive mode. ', + name: 'zeek.ftp.passive', + type: 'boolean', + }, + 'zeek.ftp.capture_password': { + category: 'zeek', + description: 'Determines if the password will be captured for this request. ', + name: 'zeek.ftp.capture_password', + type: 'boolean', + }, + 'zeek.ftp.last_auth_requested': { + category: 'zeek', + description: + 'present if base/protocols/ftp/gridftp.bro is loaded. Last authentication/security mechanism that was used. ', + name: 'zeek.ftp.last_auth_requested', + type: 'keyword', + }, + 'zeek.http.trans_depth': { + category: 'zeek', + description: + 'Represents the pipelined depth into the connection of this request/response transaction. ', + name: 'zeek.http.trans_depth', + type: 'integer', + }, + 'zeek.http.status_msg': { + category: 'zeek', + description: 'Status message returned by the server. ', + name: 'zeek.http.status_msg', + type: 'keyword', + }, + 'zeek.http.info_code': { + category: 'zeek', + description: 'Last seen 1xx informational reply code returned by the server. ', + name: 'zeek.http.info_code', + type: 'integer', + }, + 'zeek.http.info_msg': { + category: 'zeek', + description: 'Last seen 1xx informational reply message returned by the server. ', + name: 'zeek.http.info_msg', + type: 'keyword', + }, + 'zeek.http.tags': { + category: 'zeek', + description: + 'A set of indicators of various attributes discovered and related to a particular request/response pair. ', + name: 'zeek.http.tags', + type: 'keyword', + }, + 'zeek.http.password': { + category: 'zeek', + description: 'Password if basic-auth is performed for the request. ', + name: 'zeek.http.password', + type: 'keyword', + }, + 'zeek.http.captured_password': { + category: 'zeek', + description: 'Determines if the password will be captured for this request. ', + name: 'zeek.http.captured_password', + type: 'boolean', + }, + 'zeek.http.proxied': { + category: 'zeek', + description: 'All of the headers that may indicate if the HTTP request was proxied. ', + name: 'zeek.http.proxied', + type: 'keyword', + }, + 'zeek.http.range_request': { + category: 'zeek', + description: 'Indicates if this request can assume 206 partial content in response. ', + name: 'zeek.http.range_request', + type: 'boolean', + }, + 'zeek.http.client_header_names': { + category: 'zeek', + description: + 'The vector of HTTP header names sent by the client. No header values are included here, just the header names. ', + name: 'zeek.http.client_header_names', + type: 'keyword', + }, + 'zeek.http.server_header_names': { + category: 'zeek', + description: + 'The vector of HTTP header names sent by the server. No header values are included here, just the header names. ', + name: 'zeek.http.server_header_names', + type: 'keyword', + }, + 'zeek.http.orig_fuids': { + category: 'zeek', + description: 'An ordered vector of file unique IDs from the originator. ', + name: 'zeek.http.orig_fuids', + type: 'keyword', + }, + 'zeek.http.orig_mime_types': { + category: 'zeek', + description: 'An ordered vector of mime types from the originator. ', + name: 'zeek.http.orig_mime_types', + type: 'keyword', + }, + 'zeek.http.orig_filenames': { + category: 'zeek', + description: 'An ordered vector of filenames from the originator. ', + name: 'zeek.http.orig_filenames', + type: 'keyword', + }, + 'zeek.http.resp_fuids': { + category: 'zeek', + description: 'An ordered vector of file unique IDs from the responder. ', + name: 'zeek.http.resp_fuids', + type: 'keyword', + }, + 'zeek.http.resp_mime_types': { + category: 'zeek', + description: 'An ordered vector of mime types from the responder. ', + name: 'zeek.http.resp_mime_types', + type: 'keyword', + }, + 'zeek.http.resp_filenames': { + category: 'zeek', + description: 'An ordered vector of filenames from the responder. ', + name: 'zeek.http.resp_filenames', + type: 'keyword', + }, + 'zeek.http.orig_mime_depth': { + category: 'zeek', + description: 'Current number of MIME entities in the HTTP request message body. ', + name: 'zeek.http.orig_mime_depth', + type: 'integer', + }, + 'zeek.http.resp_mime_depth': { + category: 'zeek', + description: 'Current number of MIME entities in the HTTP response message body. ', + name: 'zeek.http.resp_mime_depth', + type: 'integer', + }, + 'zeek.intel.seen.indicator': { + category: 'zeek', + description: 'The intelligence indicator. ', + name: 'zeek.intel.seen.indicator', + type: 'keyword', + }, + 'zeek.intel.seen.indicator_type': { + category: 'zeek', + description: 'The type of data the indicator represents. ', + name: 'zeek.intel.seen.indicator_type', + type: 'keyword', + }, + 'zeek.intel.seen.host': { + category: 'zeek', + description: 'If the indicator type was Intel::ADDR, then this field will be present. ', + name: 'zeek.intel.seen.host', + type: 'keyword', + }, + 'zeek.intel.seen.conn': { + category: 'zeek', + description: + 'If the data was discovered within a connection, the connection record should go here to give context to the data. ', + name: 'zeek.intel.seen.conn', + type: 'keyword', + }, + 'zeek.intel.seen.where': { + category: 'zeek', + description: 'Where the data was discovered. ', + name: 'zeek.intel.seen.where', + type: 'keyword', + }, + 'zeek.intel.seen.node': { + category: 'zeek', + description: 'The name of the node where the match was discovered. ', + name: 'zeek.intel.seen.node', + type: 'keyword', + }, + 'zeek.intel.seen.uid': { + category: 'zeek', + description: + 'If the data was discovered within a connection, the connection uid should go here to give context to the data. If the conn field is provided, this will be automatically filled out. ', + name: 'zeek.intel.seen.uid', + type: 'keyword', + }, + 'zeek.intel.seen.f': { + category: 'zeek', + description: + 'If the data was discovered within a file, the file record should go here to provide context to the data. ', + name: 'zeek.intel.seen.f', + type: 'object', + }, + 'zeek.intel.seen.fuid': { + category: 'zeek', + description: + 'If the data was discovered within a file, the file uid should go here to provide context to the data. If the file record f is provided, this will be automatically filled out. ', + name: 'zeek.intel.seen.fuid', + type: 'keyword', + }, + 'zeek.intel.matched': { + category: 'zeek', + description: 'Event to represent a match in the intelligence data from data that was seen. ', + name: 'zeek.intel.matched', + type: 'keyword', + }, + 'zeek.intel.sources': { + category: 'zeek', + description: 'Sources which supplied data for this match. ', + name: 'zeek.intel.sources', + type: 'keyword', + }, + 'zeek.intel.fuid': { + category: 'zeek', + description: + 'If a file was associated with this intelligence hit, this is the uid for the file. ', + name: 'zeek.intel.fuid', + type: 'keyword', + }, + 'zeek.intel.file_mime_type': { + category: 'zeek', + description: + 'A mime type if the intelligence hit is related to a file. If the $f field is provided this will be automatically filled out. ', + name: 'zeek.intel.file_mime_type', + type: 'keyword', + }, + 'zeek.intel.file_desc': { + category: 'zeek', + description: + 'Frequently files can be described to give a bit more context. If the $f field is provided this field will be automatically filled out. ', + name: 'zeek.intel.file_desc', + type: 'keyword', + }, + 'zeek.irc.nick': { + category: 'zeek', + description: 'Nickname given for the connection. ', + name: 'zeek.irc.nick', + type: 'keyword', + }, + 'zeek.irc.user': { + category: 'zeek', + description: 'Username given for the connection. ', + name: 'zeek.irc.user', + type: 'keyword', + }, + 'zeek.irc.command': { + category: 'zeek', + description: 'Command given by the client. ', + name: 'zeek.irc.command', + type: 'keyword', + }, + 'zeek.irc.value': { + category: 'zeek', + description: 'Value for the command given by the client. ', + name: 'zeek.irc.value', + type: 'keyword', + }, + 'zeek.irc.addl': { + category: 'zeek', + description: 'Any additional data for the command. ', + name: 'zeek.irc.addl', + type: 'keyword', + }, + 'zeek.irc.dcc.file.name': { + category: 'zeek', + description: 'Present if base/protocols/irc/dcc-send.bro is loaded. DCC filename requested. ', + name: 'zeek.irc.dcc.file.name', + type: 'keyword', + }, + 'zeek.irc.dcc.file.size': { + category: 'zeek', + description: + 'Present if base/protocols/irc/dcc-send.bro is loaded. Size of the DCC transfer as indicated by the sender. ', + name: 'zeek.irc.dcc.file.size', + type: 'long', + }, + 'zeek.irc.dcc.mime_type': { + category: 'zeek', + description: + 'present if base/protocols/irc/dcc-send.bro is loaded. Sniffed mime type of the file. ', + name: 'zeek.irc.dcc.mime_type', + type: 'keyword', + }, + 'zeek.irc.fuid': { + category: 'zeek', + description: 'present if base/protocols/irc/files.bro is loaded. File unique ID. ', + name: 'zeek.irc.fuid', + type: 'keyword', + }, + 'zeek.kerberos.request_type': { + category: 'zeek', + description: 'Request type - Authentication Service (AS) or Ticket Granting Service (TGS). ', + name: 'zeek.kerberos.request_type', + type: 'keyword', + }, + 'zeek.kerberos.client': { + category: 'zeek', + description: 'Client name. ', + name: 'zeek.kerberos.client', + type: 'keyword', + }, + 'zeek.kerberos.service': { + category: 'zeek', + description: 'Service name. ', + name: 'zeek.kerberos.service', + type: 'keyword', + }, + 'zeek.kerberos.success': { + category: 'zeek', + description: 'Request result. ', + name: 'zeek.kerberos.success', + type: 'boolean', + }, + 'zeek.kerberos.error.code': { + category: 'zeek', + description: 'Error code. ', + name: 'zeek.kerberos.error.code', + type: 'integer', + }, + 'zeek.kerberos.error.msg': { + category: 'zeek', + description: 'Error message. ', + name: 'zeek.kerberos.error.msg', + type: 'keyword', + }, + 'zeek.kerberos.valid.from': { + category: 'zeek', + description: 'Ticket valid from. ', + name: 'zeek.kerberos.valid.from', + type: 'date', + }, + 'zeek.kerberos.valid.until': { + category: 'zeek', + description: 'Ticket valid until. ', + name: 'zeek.kerberos.valid.until', + type: 'date', + }, + 'zeek.kerberos.valid.days': { + category: 'zeek', + description: 'Number of days the ticket is valid for. ', + name: 'zeek.kerberos.valid.days', + type: 'integer', + }, + 'zeek.kerberos.cipher': { + category: 'zeek', + description: 'Ticket encryption type. ', + name: 'zeek.kerberos.cipher', + type: 'keyword', + }, + 'zeek.kerberos.forwardable': { + category: 'zeek', + description: 'Forwardable ticket requested. ', + name: 'zeek.kerberos.forwardable', + type: 'boolean', + }, + 'zeek.kerberos.renewable': { + category: 'zeek', + description: 'Renewable ticket requested. ', + name: 'zeek.kerberos.renewable', + type: 'boolean', + }, + 'zeek.kerberos.ticket.auth': { + category: 'zeek', + description: 'Hash of ticket used to authorize request/transaction. ', + name: 'zeek.kerberos.ticket.auth', + type: 'keyword', + }, + 'zeek.kerberos.ticket.new': { + category: 'zeek', + description: 'Hash of ticket returned by the KDC. ', + name: 'zeek.kerberos.ticket.new', + type: 'keyword', + }, + 'zeek.kerberos.cert.client.value': { + category: 'zeek', + description: 'Client certificate. ', + name: 'zeek.kerberos.cert.client.value', + type: 'keyword', + }, + 'zeek.kerberos.cert.client.fuid': { + category: 'zeek', + description: 'File unique ID of client cert. ', + name: 'zeek.kerberos.cert.client.fuid', + type: 'keyword', + }, + 'zeek.kerberos.cert.client.subject': { + category: 'zeek', + description: 'Subject of client certificate. ', + name: 'zeek.kerberos.cert.client.subject', + type: 'keyword', + }, + 'zeek.kerberos.cert.server.value': { + category: 'zeek', + description: 'Server certificate. ', + name: 'zeek.kerberos.cert.server.value', + type: 'keyword', + }, + 'zeek.kerberos.cert.server.fuid': { + category: 'zeek', + description: 'File unique ID of server certificate. ', + name: 'zeek.kerberos.cert.server.fuid', + type: 'keyword', + }, + 'zeek.kerberos.cert.server.subject': { + category: 'zeek', + description: 'Subject of server certificate. ', + name: 'zeek.kerberos.cert.server.subject', + type: 'keyword', + }, + 'zeek.modbus.function': { + category: 'zeek', + description: 'The name of the function message that was sent. ', + name: 'zeek.modbus.function', + type: 'keyword', + }, + 'zeek.modbus.exception': { + category: 'zeek', + description: 'The exception if the response was a failure. ', + name: 'zeek.modbus.exception', + type: 'keyword', + }, + 'zeek.modbus.track_address': { + category: 'zeek', + description: + 'Present if policy/protocols/modbus/track-memmap.bro is loaded. Modbus track address. ', + name: 'zeek.modbus.track_address', + type: 'integer', + }, + 'zeek.mysql.cmd': { + category: 'zeek', + description: 'The command that was issued. ', + name: 'zeek.mysql.cmd', + type: 'keyword', + }, + 'zeek.mysql.arg': { + category: 'zeek', + description: 'The argument issued to the command. ', + name: 'zeek.mysql.arg', + type: 'keyword', + }, + 'zeek.mysql.success': { + category: 'zeek', + description: 'Whether the command succeeded. ', + name: 'zeek.mysql.success', + type: 'boolean', + }, + 'zeek.mysql.rows': { + category: 'zeek', + description: 'The number of affected rows, if any. ', + name: 'zeek.mysql.rows', + type: 'integer', + }, + 'zeek.mysql.response': { + category: 'zeek', + description: 'Server message, if any. ', + name: 'zeek.mysql.response', + type: 'keyword', + }, + 'zeek.notice.connection_id': { + category: 'zeek', + description: 'Identifier of the related connection session. ', + name: 'zeek.notice.connection_id', + type: 'keyword', + }, + 'zeek.notice.icmp_id': { + category: 'zeek', + description: 'Identifier of the related ICMP session. ', + name: 'zeek.notice.icmp_id', + type: 'keyword', + }, + 'zeek.notice.file.id': { + category: 'zeek', + description: 'An identifier associated with a single file that is related to this notice. ', + name: 'zeek.notice.file.id', + type: 'keyword', + }, + 'zeek.notice.file.parent_id': { + category: 'zeek', + description: 'Identifier associated with a container file from which this one was extracted. ', + name: 'zeek.notice.file.parent_id', + type: 'keyword', + }, + 'zeek.notice.file.source': { + category: 'zeek', + description: + 'An identification of the source of the file data. E.g. it may be a network protocol over which it was transferred, or a local file path which was read, or some other input source. ', + name: 'zeek.notice.file.source', + type: 'keyword', + }, + 'zeek.notice.file.mime_type': { + category: 'zeek', + description: 'A mime type if the notice is related to a file. ', + name: 'zeek.notice.file.mime_type', + type: 'keyword', + }, + 'zeek.notice.file.is_orig': { + category: 'zeek', + description: + 'If the source of this file is a network connection, this field indicates if the file is being sent by the originator of the connection or the responder. ', + name: 'zeek.notice.file.is_orig', + type: 'boolean', + }, + 'zeek.notice.file.seen_bytes': { + category: 'zeek', + description: 'Number of bytes provided to the file analysis engine for the file. ', + name: 'zeek.notice.file.seen_bytes', + type: 'long', + }, + 'zeek.notice.ffile.total_bytes': { + category: 'zeek', + description: 'Total number of bytes that are supposed to comprise the full file. ', + name: 'zeek.notice.ffile.total_bytes', + type: 'long', + }, + 'zeek.notice.file.missing_bytes': { + category: 'zeek', + description: + 'The number of bytes in the file stream that were completely missed during the process of analysis. ', + name: 'zeek.notice.file.missing_bytes', + type: 'long', + }, + 'zeek.notice.file.overflow_bytes': { + category: 'zeek', + description: + "The number of bytes in the file stream that were not delivered to stream file analyzers. This could be overlapping bytes or bytes that couldn't be reassembled. ", + name: 'zeek.notice.file.overflow_bytes', + type: 'long', + }, + 'zeek.notice.fuid': { + category: 'zeek', + description: 'A file unique ID if this notice is related to a file. ', + name: 'zeek.notice.fuid', + type: 'keyword', + }, + 'zeek.notice.note': { + category: 'zeek', + description: 'The type of the notice. ', + name: 'zeek.notice.note', + type: 'keyword', + }, + 'zeek.notice.msg': { + category: 'zeek', + description: 'The human readable message for the notice. ', + name: 'zeek.notice.msg', + type: 'keyword', + }, + 'zeek.notice.sub': { + category: 'zeek', + description: 'The human readable sub-message. ', + name: 'zeek.notice.sub', + type: 'keyword', + }, + 'zeek.notice.n': { + category: 'zeek', + description: 'Associated count, or a status code. ', + name: 'zeek.notice.n', + type: 'long', + }, + 'zeek.notice.peer_name': { + category: 'zeek', + description: 'Name of remote peer that raised this notice. ', + name: 'zeek.notice.peer_name', + type: 'keyword', + }, + 'zeek.notice.peer_descr': { + category: 'zeek', + description: 'Textual description for the peer that raised this notice. ', + name: 'zeek.notice.peer_descr', + type: 'text', + }, + 'zeek.notice.actions': { + category: 'zeek', + description: 'The actions which have been applied to this notice. ', + name: 'zeek.notice.actions', + type: 'keyword', + }, + 'zeek.notice.email_body_sections': { + category: 'zeek', + description: + 'By adding chunks of text into this element, other scripts can expand on notices that are being emailed. ', + name: 'zeek.notice.email_body_sections', + type: 'text', + }, + 'zeek.notice.email_delay_tokens': { + category: 'zeek', + description: + 'Adding a string token to this set will cause the built-in emailing functionality to delay sending the email either the token has been removed or the email has been delayed for the specified time duration. ', + name: 'zeek.notice.email_delay_tokens', + type: 'keyword', + }, + 'zeek.notice.identifier': { + category: 'zeek', + description: + 'This field is provided when a notice is generated for the purpose of deduplicating notices. ', + name: 'zeek.notice.identifier', + type: 'keyword', + }, + 'zeek.notice.suppress_for': { + category: 'zeek', + description: + 'This field indicates the length of time that this unique notice should be suppressed. ', + name: 'zeek.notice.suppress_for', + type: 'double', + }, + 'zeek.notice.dropped': { + category: 'zeek', + description: 'Indicate if the source IP address was dropped and denied network access. ', + name: 'zeek.notice.dropped', + type: 'boolean', + }, + 'zeek.ntlm.domain': { + category: 'zeek', + description: 'Domain name given by the client. ', + name: 'zeek.ntlm.domain', + type: 'keyword', + }, + 'zeek.ntlm.hostname': { + category: 'zeek', + description: 'Hostname given by the client. ', + name: 'zeek.ntlm.hostname', + type: 'keyword', + }, + 'zeek.ntlm.success': { + category: 'zeek', + description: 'Indicate whether or not the authentication was successful. ', + name: 'zeek.ntlm.success', + type: 'boolean', + }, + 'zeek.ntlm.username': { + category: 'zeek', + description: 'Username given by the client. ', + name: 'zeek.ntlm.username', + type: 'keyword', + }, + 'zeek.ntlm.server.name.dns': { + category: 'zeek', + description: 'DNS name given by the server in a CHALLENGE. ', + name: 'zeek.ntlm.server.name.dns', + type: 'keyword', + }, + 'zeek.ntlm.server.name.netbios': { + category: 'zeek', + description: 'NetBIOS name given by the server in a CHALLENGE. ', + name: 'zeek.ntlm.server.name.netbios', + type: 'keyword', + }, + 'zeek.ntlm.server.name.tree': { + category: 'zeek', + description: 'Tree name given by the server in a CHALLENGE. ', + name: 'zeek.ntlm.server.name.tree', + type: 'keyword', + }, + 'zeek.ocsp.file_id': { + category: 'zeek', + description: 'File id of the OCSP reply. ', + name: 'zeek.ocsp.file_id', + type: 'keyword', + }, + 'zeek.ocsp.hash.algorithm': { + category: 'zeek', + description: 'Hash algorithm used to generate issuerNameHash and issuerKeyHash. ', + name: 'zeek.ocsp.hash.algorithm', + type: 'keyword', + }, + 'zeek.ocsp.hash.issuer.name': { + category: 'zeek', + description: "Hash of the issuer's distingueshed name. ", + name: 'zeek.ocsp.hash.issuer.name', + type: 'keyword', + }, + 'zeek.ocsp.hash.issuer.key': { + category: 'zeek', + description: "Hash of the issuer's public key. ", + name: 'zeek.ocsp.hash.issuer.key', + type: 'keyword', + }, + 'zeek.ocsp.serial_number': { + category: 'zeek', + description: 'Serial number of the affected certificate. ', + name: 'zeek.ocsp.serial_number', + type: 'keyword', + }, + 'zeek.ocsp.status': { + category: 'zeek', + description: 'Status of the affected certificate. ', + name: 'zeek.ocsp.status', + type: 'keyword', + }, + 'zeek.ocsp.revoke.time': { + category: 'zeek', + description: 'Time at which the certificate was revoked. ', + name: 'zeek.ocsp.revoke.time', + type: 'date', + }, + 'zeek.ocsp.revoke.reason': { + category: 'zeek', + description: 'Reason for which the certificate was revoked. ', + name: 'zeek.ocsp.revoke.reason', + type: 'keyword', + }, + 'zeek.ocsp.update.this': { + category: 'zeek', + description: 'The time at which the status being shows is known to have been correct. ', + name: 'zeek.ocsp.update.this', + type: 'date', + }, + 'zeek.ocsp.update.next': { + category: 'zeek', + description: + 'The latest time at which new information about the status of the certificate will be available. ', + name: 'zeek.ocsp.update.next', + type: 'date', + }, + 'zeek.pe.client': { + category: 'zeek', + description: "The client's version string. ", + name: 'zeek.pe.client', + type: 'keyword', + }, + 'zeek.pe.id': { + category: 'zeek', + description: 'File id of this portable executable file. ', + name: 'zeek.pe.id', + type: 'keyword', + }, + 'zeek.pe.machine': { + category: 'zeek', + description: 'The target machine that the file was compiled for. ', + name: 'zeek.pe.machine', + type: 'keyword', + }, + 'zeek.pe.compile_time': { + category: 'zeek', + description: 'The time that the file was created at. ', + name: 'zeek.pe.compile_time', + type: 'date', + }, + 'zeek.pe.os': { + category: 'zeek', + description: 'The required operating system. ', + name: 'zeek.pe.os', + type: 'keyword', + }, + 'zeek.pe.subsystem': { + category: 'zeek', + description: 'The subsystem that is required to run this file. ', + name: 'zeek.pe.subsystem', + type: 'keyword', + }, + 'zeek.pe.is_exe': { + category: 'zeek', + description: 'Is the file an executable, or just an object file? ', + name: 'zeek.pe.is_exe', + type: 'boolean', + }, + 'zeek.pe.is_64bit': { + category: 'zeek', + description: 'Is the file a 64-bit executable? ', + name: 'zeek.pe.is_64bit', + type: 'boolean', + }, + 'zeek.pe.uses_aslr': { + category: 'zeek', + description: 'Does the file support Address Space Layout Randomization? ', + name: 'zeek.pe.uses_aslr', + type: 'boolean', + }, + 'zeek.pe.uses_dep': { + category: 'zeek', + description: 'Does the file support Data Execution Prevention? ', + name: 'zeek.pe.uses_dep', + type: 'boolean', + }, + 'zeek.pe.uses_code_integrity': { + category: 'zeek', + description: 'Does the file enforce code integrity checks? ', + name: 'zeek.pe.uses_code_integrity', + type: 'boolean', + }, + 'zeek.pe.uses_seh': { + category: 'zeek', + description: 'Does the file use structured exception handing? ', + name: 'zeek.pe.uses_seh', + type: 'boolean', + }, + 'zeek.pe.has_import_table': { + category: 'zeek', + description: 'Does the file have an import table? ', + name: 'zeek.pe.has_import_table', + type: 'boolean', + }, + 'zeek.pe.has_export_table': { + category: 'zeek', + description: 'Does the file have an export table? ', + name: 'zeek.pe.has_export_table', + type: 'boolean', + }, + 'zeek.pe.has_cert_table': { + category: 'zeek', + description: 'Does the file have an attribute certificate table? ', + name: 'zeek.pe.has_cert_table', + type: 'boolean', + }, + 'zeek.pe.has_debug_data': { + category: 'zeek', + description: 'Does the file have a debug table? ', + name: 'zeek.pe.has_debug_data', + type: 'boolean', + }, + 'zeek.pe.section_names': { + category: 'zeek', + description: 'The names of the sections, in order. ', + name: 'zeek.pe.section_names', + type: 'keyword', + }, + 'zeek.radius.username': { + category: 'zeek', + description: 'The username, if present. ', + name: 'zeek.radius.username', + type: 'keyword', + }, + 'zeek.radius.mac': { + category: 'zeek', + description: 'MAC address, if present. ', + name: 'zeek.radius.mac', + type: 'keyword', + }, + 'zeek.radius.framed_addr': { + category: 'zeek', + description: + 'The address given to the network access server, if present. This is only a hint from the RADIUS server and the network access server is not required to honor the address. ', + name: 'zeek.radius.framed_addr', + type: 'ip', + }, + 'zeek.radius.remote_ip': { + category: 'zeek', + description: + 'Remote IP address, if present. This is collected from the Tunnel-Client-Endpoint attribute. ', + name: 'zeek.radius.remote_ip', + type: 'ip', + }, + 'zeek.radius.connect_info': { + category: 'zeek', + description: 'Connect info, if present. ', + name: 'zeek.radius.connect_info', + type: 'keyword', + }, + 'zeek.radius.reply_msg': { + category: 'zeek', + description: + 'Reply message from the server challenge. This is frequently shown to the user authenticating. ', + name: 'zeek.radius.reply_msg', + type: 'keyword', + }, + 'zeek.radius.result': { + category: 'zeek', + description: 'Successful or failed authentication. ', + name: 'zeek.radius.result', + type: 'keyword', + }, + 'zeek.radius.ttl': { + category: 'zeek', + description: + 'The duration between the first request and either the "Access-Accept" message or an error. If the field is empty, it means that either the request or response was not seen. ', + name: 'zeek.radius.ttl', + type: 'integer', + }, + 'zeek.radius.logged': { + category: 'zeek', + description: 'Whether this has already been logged and can be ignored. ', + name: 'zeek.radius.logged', + type: 'boolean', + }, + 'zeek.rdp.cookie': { + category: 'zeek', + description: 'Cookie value used by the client machine. This is typically a username. ', + name: 'zeek.rdp.cookie', + type: 'keyword', + }, + 'zeek.rdp.result': { + category: 'zeek', + description: + "Status result for the connection. It's a mix between RDP negotation failure messages and GCC server create response messages. ", + name: 'zeek.rdp.result', + type: 'keyword', + }, + 'zeek.rdp.security_protocol': { + category: 'zeek', + description: 'Security protocol chosen by the server. ', + name: 'zeek.rdp.security_protocol', + type: 'keyword', + }, + 'zeek.rdp.keyboard_layout': { + category: 'zeek', + description: 'Keyboard layout (language) of the client machine. ', + name: 'zeek.rdp.keyboard_layout', + type: 'keyword', + }, + 'zeek.rdp.client.build': { + category: 'zeek', + description: 'RDP client version used by the client machine. ', + name: 'zeek.rdp.client.build', + type: 'keyword', + }, + 'zeek.rdp.client.client_name': { + category: 'zeek', + description: 'Name of the client machine. ', + name: 'zeek.rdp.client.client_name', + type: 'keyword', + }, + 'zeek.rdp.client.product_id': { + category: 'zeek', + description: 'Product ID of the client machine. ', + name: 'zeek.rdp.client.product_id', + type: 'keyword', + }, + 'zeek.rdp.desktop.width': { + category: 'zeek', + description: 'Desktop width of the client machine. ', + name: 'zeek.rdp.desktop.width', + type: 'integer', + }, + 'zeek.rdp.desktop.height': { + category: 'zeek', + description: 'Desktop height of the client machine. ', + name: 'zeek.rdp.desktop.height', + type: 'integer', + }, + 'zeek.rdp.desktop.color_depth': { + category: 'zeek', + description: 'The color depth requested by the client in the high_color_depth field. ', + name: 'zeek.rdp.desktop.color_depth', + type: 'keyword', + }, + 'zeek.rdp.cert.type': { + category: 'zeek', + description: + 'If the connection is being encrypted with native RDP encryption, this is the type of cert being used. ', + name: 'zeek.rdp.cert.type', + type: 'keyword', + }, + 'zeek.rdp.cert.count': { + category: 'zeek', + description: 'The number of certs seen. X.509 can transfer an entire certificate chain. ', + name: 'zeek.rdp.cert.count', + type: 'integer', + }, + 'zeek.rdp.cert.permanent': { + category: 'zeek', + description: + 'Indicates if the provided certificate or certificate chain is permanent or temporary. ', + name: 'zeek.rdp.cert.permanent', + type: 'boolean', + }, + 'zeek.rdp.encryption.level': { + category: 'zeek', + description: 'Encryption level of the connection. ', + name: 'zeek.rdp.encryption.level', + type: 'keyword', + }, + 'zeek.rdp.encryption.method': { + category: 'zeek', + description: 'Encryption method of the connection. ', + name: 'zeek.rdp.encryption.method', + type: 'keyword', + }, + 'zeek.rdp.done': { + category: 'zeek', + description: 'Track status of logging RDP connections. ', + name: 'zeek.rdp.done', + type: 'boolean', + }, + 'zeek.rdp.ssl': { + category: 'zeek', + description: + '(present if policy/protocols/rdp/indicate_ssl.bro is loaded) Flag the connection if it was seen over SSL. ', + name: 'zeek.rdp.ssl', + type: 'boolean', + }, + 'zeek.rfb.version.client.major': { + category: 'zeek', + description: 'Major version of the client. ', + name: 'zeek.rfb.version.client.major', + type: 'keyword', + }, + 'zeek.rfb.version.client.minor': { + category: 'zeek', + description: 'Minor version of the client. ', + name: 'zeek.rfb.version.client.minor', + type: 'keyword', + }, + 'zeek.rfb.version.server.major': { + category: 'zeek', + description: 'Major version of the server. ', + name: 'zeek.rfb.version.server.major', + type: 'keyword', + }, + 'zeek.rfb.version.server.minor': { + category: 'zeek', + description: 'Minor version of the server. ', + name: 'zeek.rfb.version.server.minor', + type: 'keyword', + }, + 'zeek.rfb.auth.success': { + category: 'zeek', + description: 'Whether or not authentication was successful. ', + name: 'zeek.rfb.auth.success', + type: 'boolean', + }, + 'zeek.rfb.auth.method': { + category: 'zeek', + description: 'Identifier of authentication method used. ', + name: 'zeek.rfb.auth.method', + type: 'keyword', + }, + 'zeek.rfb.share_flag': { + category: 'zeek', + description: 'Whether the client has an exclusive or a shared session. ', + name: 'zeek.rfb.share_flag', + type: 'boolean', + }, + 'zeek.rfb.desktop_name': { + category: 'zeek', + description: 'Name of the screen that is being shared. ', + name: 'zeek.rfb.desktop_name', + type: 'keyword', + }, + 'zeek.rfb.width': { + category: 'zeek', + description: 'Width of the screen that is being shared. ', + name: 'zeek.rfb.width', + type: 'integer', + }, + 'zeek.rfb.height': { + category: 'zeek', + description: 'Height of the screen that is being shared. ', + name: 'zeek.rfb.height', + type: 'integer', + }, + 'zeek.sip.transaction_depth': { + category: 'zeek', + description: + 'Represents the pipelined depth into the connection of this request/response transaction. ', + name: 'zeek.sip.transaction_depth', + type: 'integer', + }, + 'zeek.sip.sequence.method': { + category: 'zeek', + description: 'Verb used in the SIP request (INVITE, REGISTER etc.). ', + name: 'zeek.sip.sequence.method', + type: 'keyword', + }, + 'zeek.sip.sequence.number': { + category: 'zeek', + description: 'Contents of the CSeq: header from the client. ', + name: 'zeek.sip.sequence.number', + type: 'keyword', + }, + 'zeek.sip.uri': { + category: 'zeek', + description: 'URI used in the request. ', + name: 'zeek.sip.uri', + type: 'keyword', + }, + 'zeek.sip.date': { + category: 'zeek', + description: 'Contents of the Date: header from the client. ', + name: 'zeek.sip.date', + type: 'keyword', + }, + 'zeek.sip.request.from': { + category: 'zeek', + description: + "Contents of the request From: header Note: The tag= value that's usually appended to the sender is stripped off and not logged. ", + name: 'zeek.sip.request.from', + type: 'keyword', + }, + 'zeek.sip.request.to': { + category: 'zeek', + description: 'Contents of the To: header. ', + name: 'zeek.sip.request.to', + type: 'keyword', + }, + 'zeek.sip.request.path': { + category: 'zeek', + description: 'The client message transmission path, as extracted from the headers. ', + name: 'zeek.sip.request.path', + type: 'keyword', + }, + 'zeek.sip.request.body_length': { + category: 'zeek', + description: 'Contents of the Content-Length: header from the client. ', + name: 'zeek.sip.request.body_length', + type: 'long', + }, + 'zeek.sip.response.from': { + category: 'zeek', + description: + "Contents of the response From: header Note: The tag= value that's usually appended to the sender is stripped off and not logged. ", + name: 'zeek.sip.response.from', + type: 'keyword', + }, + 'zeek.sip.response.to': { + category: 'zeek', + description: 'Contents of the response To: header. ', + name: 'zeek.sip.response.to', + type: 'keyword', + }, + 'zeek.sip.response.path': { + category: 'zeek', + description: 'The server message transmission path, as extracted from the headers. ', + name: 'zeek.sip.response.path', + type: 'keyword', + }, + 'zeek.sip.response.body_length': { + category: 'zeek', + description: 'Contents of the Content-Length: header from the server. ', + name: 'zeek.sip.response.body_length', + type: 'long', + }, + 'zeek.sip.reply_to': { + category: 'zeek', + description: 'Contents of the Reply-To: header. ', + name: 'zeek.sip.reply_to', + type: 'keyword', + }, + 'zeek.sip.call_id': { + category: 'zeek', + description: 'Contents of the Call-ID: header from the client. ', + name: 'zeek.sip.call_id', + type: 'keyword', + }, + 'zeek.sip.subject': { + category: 'zeek', + description: 'Contents of the Subject: header from the client. ', + name: 'zeek.sip.subject', + type: 'keyword', + }, + 'zeek.sip.user_agent': { + category: 'zeek', + description: 'Contents of the User-Agent: header from the client. ', + name: 'zeek.sip.user_agent', + type: 'keyword', + }, + 'zeek.sip.status.code': { + category: 'zeek', + description: 'Status code returned by the server. ', + name: 'zeek.sip.status.code', + type: 'integer', + }, + 'zeek.sip.status.msg': { + category: 'zeek', + description: 'Status message returned by the server. ', + name: 'zeek.sip.status.msg', + type: 'keyword', + }, + 'zeek.sip.warning': { + category: 'zeek', + description: 'Contents of the Warning: header. ', + name: 'zeek.sip.warning', + type: 'keyword', + }, + 'zeek.sip.content_type': { + category: 'zeek', + description: 'Contents of the Content-Type: header from the server. ', + name: 'zeek.sip.content_type', + type: 'keyword', + }, + 'zeek.smb_cmd.command': { + category: 'zeek', + description: 'The command sent by the client. ', + name: 'zeek.smb_cmd.command', + type: 'keyword', + }, + 'zeek.smb_cmd.sub_command': { + category: 'zeek', + description: 'The subcommand sent by the client, if present. ', + name: 'zeek.smb_cmd.sub_command', + type: 'keyword', + }, + 'zeek.smb_cmd.argument': { + category: 'zeek', + description: 'Command argument sent by the client, if any. ', + name: 'zeek.smb_cmd.argument', + type: 'keyword', + }, + 'zeek.smb_cmd.status': { + category: 'zeek', + description: "Server reply to the client's command. ", + name: 'zeek.smb_cmd.status', + type: 'keyword', + }, + 'zeek.smb_cmd.rtt': { + category: 'zeek', + description: 'Round trip time from the request to the response. ', + name: 'zeek.smb_cmd.rtt', + type: 'double', + }, + 'zeek.smb_cmd.version': { + category: 'zeek', + description: 'Version of SMB for the command. ', + name: 'zeek.smb_cmd.version', + type: 'keyword', + }, + 'zeek.smb_cmd.username': { + category: 'zeek', + description: 'Authenticated username, if available. ', + name: 'zeek.smb_cmd.username', + type: 'keyword', + }, + 'zeek.smb_cmd.tree': { + category: 'zeek', + description: + 'If this is related to a tree, this is the tree that was used for the current command. ', + name: 'zeek.smb_cmd.tree', + type: 'keyword', + }, + 'zeek.smb_cmd.tree_service': { + category: 'zeek', + description: 'The type of tree (disk share, printer share, named pipe, etc.). ', + name: 'zeek.smb_cmd.tree_service', + type: 'keyword', + }, + 'zeek.smb_cmd.file.name': { + category: 'zeek', + description: 'Filename if one was seen. ', + name: 'zeek.smb_cmd.file.name', + type: 'keyword', + }, + 'zeek.smb_cmd.file.action': { + category: 'zeek', + description: 'Action this log record represents. ', + name: 'zeek.smb_cmd.file.action', + type: 'keyword', + }, + 'zeek.smb_cmd.file.uid': { + category: 'zeek', + description: 'UID of the referenced file. ', + name: 'zeek.smb_cmd.file.uid', + type: 'keyword', + }, + 'zeek.smb_cmd.file.host.tx': { + category: 'zeek', + description: 'Address of the transmitting host. ', + name: 'zeek.smb_cmd.file.host.tx', + type: 'ip', + }, + 'zeek.smb_cmd.file.host.rx': { + category: 'zeek', + description: 'Address of the receiving host. ', + name: 'zeek.smb_cmd.file.host.rx', + type: 'ip', + }, + 'zeek.smb_cmd.smb1_offered_dialects': { + category: 'zeek', + description: + 'Present if base/protocols/smb/smb1-main.bro is loaded. Dialects offered by the client. ', + name: 'zeek.smb_cmd.smb1_offered_dialects', + type: 'keyword', + }, + 'zeek.smb_cmd.smb2_offered_dialects': { + category: 'zeek', + description: + 'Present if base/protocols/smb/smb2-main.bro is loaded. Dialects offered by the client. ', + name: 'zeek.smb_cmd.smb2_offered_dialects', + type: 'integer', + }, + 'zeek.smb_files.action': { + category: 'zeek', + description: 'Action this log record represents. ', + name: 'zeek.smb_files.action', + type: 'keyword', + }, + 'zeek.smb_files.fid': { + category: 'zeek', + description: 'ID referencing this file. ', + name: 'zeek.smb_files.fid', + type: 'integer', + }, + 'zeek.smb_files.name': { + category: 'zeek', + description: 'Filename if one was seen. ', + name: 'zeek.smb_files.name', + type: 'keyword', + }, + 'zeek.smb_files.path': { + category: 'zeek', + description: 'Path pulled from the tree this file was transferred to or from. ', + name: 'zeek.smb_files.path', + type: 'keyword', + }, + 'zeek.smb_files.previous_name': { + category: 'zeek', + description: "If the rename action was seen, this will be the file's previous name. ", + name: 'zeek.smb_files.previous_name', + type: 'keyword', + }, + 'zeek.smb_files.size': { + category: 'zeek', + description: 'Byte size of the file. ', + name: 'zeek.smb_files.size', + type: 'long', + }, + 'zeek.smb_files.times.accessed': { + category: 'zeek', + description: "The file's access time. ", + name: 'zeek.smb_files.times.accessed', + type: 'date', + }, + 'zeek.smb_files.times.changed': { + category: 'zeek', + description: "The file's change time. ", + name: 'zeek.smb_files.times.changed', + type: 'date', + }, + 'zeek.smb_files.times.created': { + category: 'zeek', + description: "The file's create time. ", + name: 'zeek.smb_files.times.created', + type: 'date', + }, + 'zeek.smb_files.times.modified': { + category: 'zeek', + description: "The file's modify time. ", + name: 'zeek.smb_files.times.modified', + type: 'date', + }, + 'zeek.smb_files.uuid': { + category: 'zeek', + description: 'UUID referencing this file if DCE/RPC. ', + name: 'zeek.smb_files.uuid', + type: 'keyword', + }, + 'zeek.smb_mapping.path': { + category: 'zeek', + description: 'Name of the tree path. ', + name: 'zeek.smb_mapping.path', + type: 'keyword', + }, + 'zeek.smb_mapping.service': { + category: 'zeek', + description: 'The type of resource of the tree (disk share, printer share, named pipe, etc.). ', + name: 'zeek.smb_mapping.service', + type: 'keyword', + }, + 'zeek.smb_mapping.native_file_system': { + category: 'zeek', + description: 'File system of the tree. ', + name: 'zeek.smb_mapping.native_file_system', + type: 'keyword', + }, + 'zeek.smb_mapping.share_type': { + category: 'zeek', + description: + 'If this is SMB2, a share type will be included. For SMB1, the type of share will be deduced and included as well. ', + name: 'zeek.smb_mapping.share_type', + type: 'keyword', + }, + 'zeek.smtp.transaction_depth': { + category: 'zeek', + description: + 'A count to represent the depth of this message transaction in a single connection where multiple messages were transferred. ', + name: 'zeek.smtp.transaction_depth', + type: 'integer', + }, + 'zeek.smtp.helo': { + category: 'zeek', + description: 'Contents of the Helo header. ', + name: 'zeek.smtp.helo', + type: 'keyword', + }, + 'zeek.smtp.mail_from': { + category: 'zeek', + description: 'Email addresses found in the MAIL FROM header. ', + name: 'zeek.smtp.mail_from', + type: 'keyword', + }, + 'zeek.smtp.rcpt_to': { + category: 'zeek', + description: 'Email addresses found in the RCPT TO header. ', + name: 'zeek.smtp.rcpt_to', + type: 'keyword', + }, + 'zeek.smtp.date': { + category: 'zeek', + description: 'Contents of the Date header. ', + name: 'zeek.smtp.date', + type: 'date', + }, + 'zeek.smtp.from': { + category: 'zeek', + description: 'Contents of the From header. ', + name: 'zeek.smtp.from', + type: 'keyword', + }, + 'zeek.smtp.to': { + category: 'zeek', + description: 'Contents of the To header. ', + name: 'zeek.smtp.to', + type: 'keyword', + }, + 'zeek.smtp.cc': { + category: 'zeek', + description: 'Contents of the CC header. ', + name: 'zeek.smtp.cc', + type: 'keyword', + }, + 'zeek.smtp.reply_to': { + category: 'zeek', + description: 'Contents of the ReplyTo header. ', + name: 'zeek.smtp.reply_to', + type: 'keyword', + }, + 'zeek.smtp.msg_id': { + category: 'zeek', + description: 'Contents of the MsgID header. ', + name: 'zeek.smtp.msg_id', + type: 'keyword', + }, + 'zeek.smtp.in_reply_to': { + category: 'zeek', + description: 'Contents of the In-Reply-To header. ', + name: 'zeek.smtp.in_reply_to', + type: 'keyword', + }, + 'zeek.smtp.subject': { + category: 'zeek', + description: 'Contents of the Subject header. ', + name: 'zeek.smtp.subject', + type: 'keyword', + }, + 'zeek.smtp.x_originating_ip': { + category: 'zeek', + description: 'Contents of the X-Originating-IP header. ', + name: 'zeek.smtp.x_originating_ip', + type: 'keyword', + }, + 'zeek.smtp.first_received': { + category: 'zeek', + description: 'Contents of the first Received header. ', + name: 'zeek.smtp.first_received', + type: 'keyword', + }, + 'zeek.smtp.second_received': { + category: 'zeek', + description: 'Contents of the second Received header. ', + name: 'zeek.smtp.second_received', + type: 'keyword', + }, + 'zeek.smtp.last_reply': { + category: 'zeek', + description: 'The last message that the server sent to the client. ', + name: 'zeek.smtp.last_reply', + type: 'keyword', + }, + 'zeek.smtp.path': { + category: 'zeek', + description: 'The message transmission path, as extracted from the headers. ', + name: 'zeek.smtp.path', + type: 'ip', + }, + 'zeek.smtp.user_agent': { + category: 'zeek', + description: 'Value of the User-Agent header from the client. ', + name: 'zeek.smtp.user_agent', + type: 'keyword', + }, + 'zeek.smtp.tls': { + category: 'zeek', + description: 'Indicates that the connection has switched to using TLS. ', + name: 'zeek.smtp.tls', + type: 'boolean', + }, + 'zeek.smtp.process_received_from': { + category: 'zeek', + description: 'Indicates if the "Received: from" headers should still be processed. ', + name: 'zeek.smtp.process_received_from', + type: 'boolean', + }, + 'zeek.smtp.has_client_activity': { + category: 'zeek', + description: 'Indicates if client activity has been seen, but not yet logged. ', + name: 'zeek.smtp.has_client_activity', + type: 'boolean', + }, + 'zeek.smtp.fuids': { + category: 'zeek', + description: + '(present if base/protocols/smtp/files.bro is loaded) An ordered vector of file unique IDs seen attached to the message. ', + name: 'zeek.smtp.fuids', + type: 'keyword', + }, + 'zeek.smtp.is_webmail': { + category: 'zeek', + description: 'Indicates if the message was sent through a webmail interface. ', + name: 'zeek.smtp.is_webmail', + type: 'boolean', + }, + 'zeek.snmp.duration': { + category: 'zeek', + description: + 'The amount of time between the first packet beloning to the SNMP session and the latest one seen. ', + name: 'zeek.snmp.duration', + type: 'double', + }, + 'zeek.snmp.version': { + category: 'zeek', + description: 'The version of SNMP being used. ', + name: 'zeek.snmp.version', + type: 'keyword', + }, + 'zeek.snmp.community': { + category: 'zeek', + description: + "The community string of the first SNMP packet associated with the session. This is used as part of SNMP's (v1 and v2c) administrative/security framework. See RFC 1157 or RFC 1901. ", + name: 'zeek.snmp.community', + type: 'keyword', + }, + 'zeek.snmp.get.requests': { + category: 'zeek', + description: + 'The number of variable bindings in GetRequest/GetNextRequest PDUs seen for the session. ', + name: 'zeek.snmp.get.requests', + type: 'integer', + }, + 'zeek.snmp.get.bulk_requests': { + category: 'zeek', + description: 'The number of variable bindings in GetBulkRequest PDUs seen for the session. ', + name: 'zeek.snmp.get.bulk_requests', + type: 'integer', + }, + 'zeek.snmp.get.responses': { + category: 'zeek', + description: + 'The number of variable bindings in GetResponse/Response PDUs seen for the session. ', + name: 'zeek.snmp.get.responses', + type: 'integer', + }, + 'zeek.snmp.set.requests': { + category: 'zeek', + description: 'The number of variable bindings in SetRequest PDUs seen for the session. ', + name: 'zeek.snmp.set.requests', + type: 'integer', + }, + 'zeek.snmp.display_string': { + category: 'zeek', + description: 'A system description of the SNMP responder endpoint. ', + name: 'zeek.snmp.display_string', + type: 'keyword', + }, + 'zeek.snmp.up_since': { + category: 'zeek', + description: "The time at which the SNMP responder endpoint claims it's been up since. ", + name: 'zeek.snmp.up_since', + type: 'date', + }, + 'zeek.socks.version': { + category: 'zeek', + description: 'Protocol version of SOCKS. ', + name: 'zeek.socks.version', + type: 'integer', + }, + 'zeek.socks.user': { + category: 'zeek', + description: 'Username used to request a login to the proxy. ', + name: 'zeek.socks.user', + type: 'keyword', + }, + 'zeek.socks.password': { + category: 'zeek', + description: 'Password used to request a login to the proxy. ', + name: 'zeek.socks.password', + type: 'keyword', + }, + 'zeek.socks.status': { + category: 'zeek', + description: 'Server status for the attempt at using the proxy. ', + name: 'zeek.socks.status', + type: 'keyword', + }, + 'zeek.socks.request.host': { + category: 'zeek', + description: 'Client requested SOCKS address. Could be an address, a name or both. ', + name: 'zeek.socks.request.host', + type: 'keyword', + }, + 'zeek.socks.request.port': { + category: 'zeek', + description: 'Client requested port. ', + name: 'zeek.socks.request.port', + type: 'integer', + }, + 'zeek.socks.bound.host': { + category: 'zeek', + description: 'Server bound address. Could be an address, a name or both. ', + name: 'zeek.socks.bound.host', + type: 'keyword', + }, + 'zeek.socks.bound.port': { + category: 'zeek', + description: 'Server bound port. ', + name: 'zeek.socks.bound.port', + type: 'integer', + }, + 'zeek.socks.capture_password': { + category: 'zeek', + description: 'Determines if the password will be captured for this request. ', + name: 'zeek.socks.capture_password', + type: 'boolean', + }, + 'zeek.ssh.client': { + category: 'zeek', + description: "The client's version string. ", + name: 'zeek.ssh.client', + type: 'keyword', + }, + 'zeek.ssh.direction': { + category: 'zeek', + description: + 'Direction of the connection. If the client was a local host logging into an external host, this would be OUTBOUND. INBOUND would be set for the opposite situation. ', + name: 'zeek.ssh.direction', + type: 'keyword', + }, + 'zeek.ssh.host_key': { + category: 'zeek', + description: "The server's key thumbprint. ", + name: 'zeek.ssh.host_key', + type: 'keyword', + }, + 'zeek.ssh.server': { + category: 'zeek', + description: "The server's version string. ", + name: 'zeek.ssh.server', + type: 'keyword', + }, + 'zeek.ssh.version': { + category: 'zeek', + description: 'SSH major version (1 or 2). ', + name: 'zeek.ssh.version', + type: 'integer', + }, + 'zeek.ssh.algorithm.cipher': { + category: 'zeek', + description: 'The encryption algorithm in use. ', + name: 'zeek.ssh.algorithm.cipher', + type: 'keyword', + }, + 'zeek.ssh.algorithm.compression': { + category: 'zeek', + description: 'The compression algorithm in use. ', + name: 'zeek.ssh.algorithm.compression', + type: 'keyword', + }, + 'zeek.ssh.algorithm.host_key': { + category: 'zeek', + description: "The server host key's algorithm. ", + name: 'zeek.ssh.algorithm.host_key', + type: 'keyword', + }, + 'zeek.ssh.algorithm.key_exchange': { + category: 'zeek', + description: 'The key exchange algorithm in use. ', + name: 'zeek.ssh.algorithm.key_exchange', + type: 'keyword', + }, + 'zeek.ssh.algorithm.mac': { + category: 'zeek', + description: 'The signing (MAC) algorithm in use. ', + name: 'zeek.ssh.algorithm.mac', + type: 'keyword', + }, + 'zeek.ssh.auth.attempts': { + category: 'zeek', + description: + "The number of authentication attemps we observed. There's always at least one, since some servers might support no authentication at all. It's important to note that not all of these are failures, since some servers require two-factor auth (e.g. password AND pubkey). ", + name: 'zeek.ssh.auth.attempts', + type: 'integer', + }, + 'zeek.ssh.auth.success': { + category: 'zeek', + description: 'Authentication result. ', + name: 'zeek.ssh.auth.success', + type: 'boolean', + }, + 'zeek.ssl.version': { + category: 'zeek', + description: 'SSL/TLS version that was logged. ', + name: 'zeek.ssl.version', + type: 'keyword', + }, + 'zeek.ssl.cipher': { + category: 'zeek', + description: 'SSL/TLS cipher suite that was logged. ', + name: 'zeek.ssl.cipher', + type: 'keyword', + }, + 'zeek.ssl.curve': { + category: 'zeek', + description: 'Elliptic curve that was logged when using ECDH/ECDHE. ', + name: 'zeek.ssl.curve', + type: 'keyword', + }, + 'zeek.ssl.resumed': { + category: 'zeek', + description: + 'Flag to indicate if the session was resumed reusing the key material exchanged in an earlier connection. ', + name: 'zeek.ssl.resumed', + type: 'boolean', + }, + 'zeek.ssl.next_protocol': { + category: 'zeek', + description: + 'Next protocol the server chose using the application layer next protocol extension. ', + name: 'zeek.ssl.next_protocol', + type: 'keyword', + }, + 'zeek.ssl.established': { + category: 'zeek', + description: 'Flag to indicate if this ssl session has been established successfully. ', + name: 'zeek.ssl.established', + type: 'boolean', + }, + 'zeek.ssl.validation.status': { + category: 'zeek', + description: 'Result of certificate validation for this connection. ', + name: 'zeek.ssl.validation.status', + type: 'keyword', + }, + 'zeek.ssl.validation.code': { + category: 'zeek', + description: + 'Result of certificate validation for this connection, given as OpenSSL validation code. ', + name: 'zeek.ssl.validation.code', + type: 'keyword', + }, + 'zeek.ssl.last_alert': { + category: 'zeek', + description: 'Last alert that was seen during the connection. ', + name: 'zeek.ssl.last_alert', + type: 'keyword', + }, + 'zeek.ssl.server.name': { + category: 'zeek', + description: + 'Value of the Server Name Indicator SSL/TLS extension. It indicates the server name that the client was requesting. ', + name: 'zeek.ssl.server.name', + type: 'keyword', + }, + 'zeek.ssl.server.cert_chain': { + category: 'zeek', + description: + 'Chain of certificates offered by the server to validate its complete signing chain. ', + name: 'zeek.ssl.server.cert_chain', + type: 'keyword', + }, + 'zeek.ssl.server.cert_chain_fuids': { + category: 'zeek', + description: + 'An ordered vector of certificate file identifiers for the certificates offered by the server. ', + name: 'zeek.ssl.server.cert_chain_fuids', + type: 'keyword', + }, + 'zeek.ssl.server.issuer.common_name': { + category: 'zeek', + description: 'Common name of the signer of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.issuer.common_name', + type: 'keyword', + }, + 'zeek.ssl.server.issuer.country': { + category: 'zeek', + description: 'Country code of the signer of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.issuer.country', + type: 'keyword', + }, + 'zeek.ssl.server.issuer.locality': { + category: 'zeek', + description: 'Locality of the signer of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.issuer.locality', + type: 'keyword', + }, + 'zeek.ssl.server.issuer.organization': { + category: 'zeek', + description: 'Organization of the signer of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.issuer.organization', + type: 'keyword', + }, + 'zeek.ssl.server.issuer.organizational_unit': { + category: 'zeek', + description: + 'Organizational unit of the signer of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.issuer.organizational_unit', + type: 'keyword', + }, + 'zeek.ssl.server.issuer.state': { + category: 'zeek', + description: + 'State or province name of the signer of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.issuer.state', + type: 'keyword', + }, + 'zeek.ssl.server.subject.common_name': { + category: 'zeek', + description: 'Common name of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.subject.common_name', + type: 'keyword', + }, + 'zeek.ssl.server.subject.country': { + category: 'zeek', + description: 'Country code of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.subject.country', + type: 'keyword', + }, + 'zeek.ssl.server.subject.locality': { + category: 'zeek', + description: 'Locality of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.subject.locality', + type: 'keyword', + }, + 'zeek.ssl.server.subject.organization': { + category: 'zeek', + description: 'Organization of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.subject.organization', + type: 'keyword', + }, + 'zeek.ssl.server.subject.organizational_unit': { + category: 'zeek', + description: 'Organizational unit of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.subject.organizational_unit', + type: 'keyword', + }, + 'zeek.ssl.server.subject.state': { + category: 'zeek', + description: 'State or province name of the X.509 certificate offered by the server. ', + name: 'zeek.ssl.server.subject.state', + type: 'keyword', + }, + 'zeek.ssl.client.cert_chain': { + category: 'zeek', + description: + 'Chain of certificates offered by the client to validate its complete signing chain. ', + name: 'zeek.ssl.client.cert_chain', + type: 'keyword', + }, + 'zeek.ssl.client.cert_chain_fuids': { + category: 'zeek', + description: + 'An ordered vector of certificate file identifiers for the certificates offered by the client. ', + name: 'zeek.ssl.client.cert_chain_fuids', + type: 'keyword', + }, + 'zeek.ssl.client.issuer.common_name': { + category: 'zeek', + description: 'Common name of the signer of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.issuer.common_name', + type: 'keyword', + }, + 'zeek.ssl.client.issuer.country': { + category: 'zeek', + description: 'Country code of the signer of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.issuer.country', + type: 'keyword', + }, + 'zeek.ssl.client.issuer.locality': { + category: 'zeek', + description: 'Locality of the signer of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.issuer.locality', + type: 'keyword', + }, + 'zeek.ssl.client.issuer.organization': { + category: 'zeek', + description: 'Organization of the signer of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.issuer.organization', + type: 'keyword', + }, + 'zeek.ssl.client.issuer.organizational_unit': { + category: 'zeek', + description: + 'Organizational unit of the signer of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.issuer.organizational_unit', + type: 'keyword', + }, + 'zeek.ssl.client.issuer.state': { + category: 'zeek', + description: + 'State or province name of the signer of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.issuer.state', + type: 'keyword', + }, + 'zeek.ssl.client.subject.common_name': { + category: 'zeek', + description: 'Common name of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.subject.common_name', + type: 'keyword', + }, + 'zeek.ssl.client.subject.country': { + category: 'zeek', + description: 'Country code of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.subject.country', + type: 'keyword', + }, + 'zeek.ssl.client.subject.locality': { + category: 'zeek', + description: 'Locality of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.subject.locality', + type: 'keyword', + }, + 'zeek.ssl.client.subject.organization': { + category: 'zeek', + description: 'Organization of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.subject.organization', + type: 'keyword', + }, + 'zeek.ssl.client.subject.organizational_unit': { + category: 'zeek', + description: 'Organizational unit of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.subject.organizational_unit', + type: 'keyword', + }, + 'zeek.ssl.client.subject.state': { + category: 'zeek', + description: 'State or province name of the X.509 certificate offered by the client. ', + name: 'zeek.ssl.client.subject.state', + type: 'keyword', + }, + 'zeek.stats.peer': { + category: 'zeek', + description: 'Peer that generated this log. Mostly for clusters. ', + name: 'zeek.stats.peer', + type: 'keyword', + }, + 'zeek.stats.memory': { + category: 'zeek', + description: 'Amount of memory currently in use in MB. ', + name: 'zeek.stats.memory', + type: 'integer', + }, + 'zeek.stats.packets.processed': { + category: 'zeek', + description: 'Number of packets processed since the last stats interval. ', + name: 'zeek.stats.packets.processed', + type: 'long', + }, + 'zeek.stats.packets.dropped': { + category: 'zeek', + description: + 'Number of packets dropped since the last stats interval if reading live traffic. ', + name: 'zeek.stats.packets.dropped', + type: 'long', + }, + 'zeek.stats.packets.received': { + category: 'zeek', + description: + 'Number of packets seen on the link since the last stats interval if reading live traffic. ', + name: 'zeek.stats.packets.received', + type: 'long', + }, + 'zeek.stats.bytes.received': { + category: 'zeek', + description: 'Number of bytes received since the last stats interval if reading live traffic. ', + name: 'zeek.stats.bytes.received', + type: 'long', + }, + 'zeek.stats.connections.tcp.active': { + category: 'zeek', + description: 'TCP connections currently in memory. ', + name: 'zeek.stats.connections.tcp.active', + type: 'integer', + }, + 'zeek.stats.connections.tcp.count': { + category: 'zeek', + description: 'TCP connections seen since last stats interval. ', + name: 'zeek.stats.connections.tcp.count', + type: 'integer', + }, + 'zeek.stats.connections.udp.active': { + category: 'zeek', + description: 'UDP connections currently in memory. ', + name: 'zeek.stats.connections.udp.active', + type: 'integer', + }, + 'zeek.stats.connections.udp.count': { + category: 'zeek', + description: 'UDP connections seen since last stats interval. ', + name: 'zeek.stats.connections.udp.count', + type: 'integer', + }, + 'zeek.stats.connections.icmp.active': { + category: 'zeek', + description: 'ICMP connections currently in memory. ', + name: 'zeek.stats.connections.icmp.active', + type: 'integer', + }, + 'zeek.stats.connections.icmp.count': { + category: 'zeek', + description: 'ICMP connections seen since last stats interval. ', + name: 'zeek.stats.connections.icmp.count', + type: 'integer', + }, + 'zeek.stats.events.processed': { + category: 'zeek', + description: 'Number of events processed since the last stats interval. ', + name: 'zeek.stats.events.processed', + type: 'integer', + }, + 'zeek.stats.events.queued': { + category: 'zeek', + description: 'Number of events that have been queued since the last stats interval. ', + name: 'zeek.stats.events.queued', + type: 'integer', + }, + 'zeek.stats.timers.count': { + category: 'zeek', + description: 'Number of timers scheduled since last stats interval. ', + name: 'zeek.stats.timers.count', + type: 'integer', + }, + 'zeek.stats.timers.active': { + category: 'zeek', + description: 'Current number of scheduled timers. ', + name: 'zeek.stats.timers.active', + type: 'integer', + }, + 'zeek.stats.files.count': { + category: 'zeek', + description: 'Number of files seen since last stats interval. ', + name: 'zeek.stats.files.count', + type: 'integer', + }, + 'zeek.stats.files.active': { + category: 'zeek', + description: 'Current number of files actively being seen. ', + name: 'zeek.stats.files.active', + type: 'integer', + }, + 'zeek.stats.dns_requests.count': { + category: 'zeek', + description: 'Number of DNS requests seen since last stats interval. ', + name: 'zeek.stats.dns_requests.count', + type: 'integer', + }, + 'zeek.stats.dns_requests.active': { + category: 'zeek', + description: 'Current number of DNS requests awaiting a reply. ', + name: 'zeek.stats.dns_requests.active', + type: 'integer', + }, + 'zeek.stats.reassembly_size.tcp': { + category: 'zeek', + description: 'Current size of TCP data in reassembly. ', + name: 'zeek.stats.reassembly_size.tcp', + type: 'integer', + }, + 'zeek.stats.reassembly_size.file': { + category: 'zeek', + description: 'Current size of File data in reassembly. ', + name: 'zeek.stats.reassembly_size.file', + type: 'integer', + }, + 'zeek.stats.reassembly_size.frag': { + category: 'zeek', + description: 'Current size of packet fragment data in reassembly. ', + name: 'zeek.stats.reassembly_size.frag', + type: 'integer', + }, + 'zeek.stats.reassembly_size.unknown': { + category: 'zeek', + description: 'Current size of unknown data in reassembly (this is only PIA buffer right now). ', + name: 'zeek.stats.reassembly_size.unknown', + type: 'integer', + }, + 'zeek.stats.timestamp_lag': { + category: 'zeek', + description: 'Lag between the wall clock and packet timestamps if reading live traffic. ', + name: 'zeek.stats.timestamp_lag', + type: 'integer', + }, + 'zeek.syslog.facility': { + category: 'zeek', + description: 'Syslog facility for the message. ', + name: 'zeek.syslog.facility', + type: 'keyword', + }, + 'zeek.syslog.severity': { + category: 'zeek', + description: 'Syslog severity for the message. ', + name: 'zeek.syslog.severity', + type: 'keyword', + }, + 'zeek.syslog.message': { + category: 'zeek', + description: 'The plain text message. ', + name: 'zeek.syslog.message', + type: 'keyword', + }, + 'zeek.tunnel.type': { + category: 'zeek', + description: 'The type of tunnel. ', + name: 'zeek.tunnel.type', + type: 'keyword', + }, + 'zeek.tunnel.action': { + category: 'zeek', + description: 'The type of activity that occurred. ', + name: 'zeek.tunnel.action', + type: 'keyword', + }, + 'zeek.weird.name': { + category: 'zeek', + description: 'The name of the weird that occurred. ', + name: 'zeek.weird.name', + type: 'keyword', + }, + 'zeek.weird.additional_info': { + category: 'zeek', + description: 'Additional information accompanying the weird if any. ', + name: 'zeek.weird.additional_info', + type: 'keyword', + }, + 'zeek.weird.notice': { + category: 'zeek', + description: 'Indicate if this weird was also turned into a notice. ', + name: 'zeek.weird.notice', + type: 'boolean', + }, + 'zeek.weird.peer': { + category: 'zeek', + description: + 'The peer that originated this weird. This is helpful in cluster deployments if a particular cluster node is having trouble to help identify which node is having trouble. ', + name: 'zeek.weird.peer', + type: 'keyword', + }, + 'zeek.weird.identifier': { + category: 'zeek', + description: + 'This field is to be provided when a weird is generated for the purpose of deduplicating weirds. The identifier string should be unique for a single instance of the weird. This field is used to define when a weird is conceptually a duplicate of a previous weird. ', + name: 'zeek.weird.identifier', + type: 'keyword', + }, + 'zeek.x509.id': { + category: 'zeek', + description: 'File id of this certificate. ', + name: 'zeek.x509.id', + type: 'keyword', + }, + 'zeek.x509.certificate.version': { + category: 'zeek', + description: 'Version number. ', + name: 'zeek.x509.certificate.version', + type: 'integer', + }, + 'zeek.x509.certificate.serial': { + category: 'zeek', + description: 'Serial number. ', + name: 'zeek.x509.certificate.serial', + type: 'keyword', + }, + 'zeek.x509.certificate.subject.country': { + category: 'zeek', + description: 'Country provided in the certificate subject. ', + name: 'zeek.x509.certificate.subject.country', + type: 'keyword', + }, + 'zeek.x509.certificate.subject.common_name': { + category: 'zeek', + description: 'Common name provided in the certificate subject. ', + name: 'zeek.x509.certificate.subject.common_name', + type: 'keyword', + }, + 'zeek.x509.certificate.subject.locality': { + category: 'zeek', + description: 'Locality provided in the certificate subject. ', + name: 'zeek.x509.certificate.subject.locality', + type: 'keyword', + }, + 'zeek.x509.certificate.subject.organization': { + category: 'zeek', + description: 'Organization provided in the certificate subject. ', + name: 'zeek.x509.certificate.subject.organization', + type: 'keyword', + }, + 'zeek.x509.certificate.subject.organizational_unit': { + category: 'zeek', + description: 'Organizational unit provided in the certificate subject. ', + name: 'zeek.x509.certificate.subject.organizational_unit', + type: 'keyword', + }, + 'zeek.x509.certificate.subject.state': { + category: 'zeek', + description: 'State or province provided in the certificate subject. ', + name: 'zeek.x509.certificate.subject.state', + type: 'keyword', + }, + 'zeek.x509.certificate.issuer.country': { + category: 'zeek', + description: 'Country provided in the certificate issuer field. ', + name: 'zeek.x509.certificate.issuer.country', + type: 'keyword', + }, + 'zeek.x509.certificate.issuer.common_name': { + category: 'zeek', + description: 'Common name provided in the certificate issuer field. ', + name: 'zeek.x509.certificate.issuer.common_name', + type: 'keyword', + }, + 'zeek.x509.certificate.issuer.locality': { + category: 'zeek', + description: 'Locality provided in the certificate issuer field. ', + name: 'zeek.x509.certificate.issuer.locality', + type: 'keyword', + }, + 'zeek.x509.certificate.issuer.organization': { + category: 'zeek', + description: 'Organization provided in the certificate issuer field. ', + name: 'zeek.x509.certificate.issuer.organization', + type: 'keyword', + }, + 'zeek.x509.certificate.issuer.organizational_unit': { + category: 'zeek', + description: 'Organizational unit provided in the certificate issuer field. ', + name: 'zeek.x509.certificate.issuer.organizational_unit', + type: 'keyword', + }, + 'zeek.x509.certificate.issuer.state': { + category: 'zeek', + description: 'State or province provided in the certificate issuer field. ', + name: 'zeek.x509.certificate.issuer.state', + type: 'keyword', + }, + 'zeek.x509.certificate.common_name': { + category: 'zeek', + description: 'Last (most specific) common name. ', + name: 'zeek.x509.certificate.common_name', + type: 'keyword', + }, + 'zeek.x509.certificate.valid.from': { + category: 'zeek', + description: 'Timestamp before when certificate is not valid. ', + name: 'zeek.x509.certificate.valid.from', + type: 'date', + }, + 'zeek.x509.certificate.valid.until': { + category: 'zeek', + description: 'Timestamp after when certificate is not valid. ', + name: 'zeek.x509.certificate.valid.until', + type: 'date', + }, + 'zeek.x509.certificate.key.algorithm': { + category: 'zeek', + description: 'Name of the key algorithm. ', + name: 'zeek.x509.certificate.key.algorithm', + type: 'keyword', + }, + 'zeek.x509.certificate.key.type': { + category: 'zeek', + description: 'Key type, if key parseable by openssl (either rsa, dsa or ec). ', + name: 'zeek.x509.certificate.key.type', + type: 'keyword', + }, + 'zeek.x509.certificate.key.length': { + category: 'zeek', + description: 'Key length in bits. ', + name: 'zeek.x509.certificate.key.length', + type: 'integer', + }, + 'zeek.x509.certificate.signature_algorithm': { + category: 'zeek', + description: 'Name of the signature algorithm. ', + name: 'zeek.x509.certificate.signature_algorithm', + type: 'keyword', + }, + 'zeek.x509.certificate.exponent': { + category: 'zeek', + description: 'Exponent, if RSA-certificate. ', + name: 'zeek.x509.certificate.exponent', + type: 'keyword', + }, + 'zeek.x509.certificate.curve': { + category: 'zeek', + description: 'Curve, if EC-certificate. ', + name: 'zeek.x509.certificate.curve', + type: 'keyword', + }, + 'zeek.x509.san.dns': { + category: 'zeek', + description: 'List of DNS entries in SAN. ', + name: 'zeek.x509.san.dns', + type: 'keyword', + }, + 'zeek.x509.san.uri': { + category: 'zeek', + description: 'List of URI entries in SAN. ', + name: 'zeek.x509.san.uri', + type: 'keyword', + }, + 'zeek.x509.san.email': { + category: 'zeek', + description: 'List of email entries in SAN. ', + name: 'zeek.x509.san.email', + type: 'keyword', + }, + 'zeek.x509.san.ip': { + category: 'zeek', + description: 'List of IP entries in SAN. ', + name: 'zeek.x509.san.ip', + type: 'ip', + }, + 'zeek.x509.san.other_fields': { + category: 'zeek', + description: 'True if the certificate contained other, not recognized or parsed name fields. ', + name: 'zeek.x509.san.other_fields', + type: 'boolean', + }, + 'zeek.x509.basic_constraints.certificate_authority': { + category: 'zeek', + description: 'CA flag set or not. ', + name: 'zeek.x509.basic_constraints.certificate_authority', + type: 'boolean', + }, + 'zeek.x509.basic_constraints.path_length': { + category: 'zeek', + description: 'Maximum path length. ', + name: 'zeek.x509.basic_constraints.path_length', + type: 'integer', + }, + 'zeek.x509.log_cert': { + category: 'zeek', + description: + 'Present if policy/protocols/ssl/log-hostcerts-only.bro is loaded Logging of certificate is suppressed if set to F. ', + name: 'zeek.x509.log_cert', + type: 'boolean', + }, + 'awscloudwatch.log_group': { + category: 'awscloudwatch', + description: 'The name of the log group to which this event belongs.', + name: 'awscloudwatch.log_group', + type: 'keyword', + }, + 'awscloudwatch.log_stream': { + category: 'awscloudwatch', + description: 'The name of the log stream to which this event belongs.', + name: 'awscloudwatch.log_stream', + type: 'keyword', + }, + 'awscloudwatch.ingestion_time': { + category: 'awscloudwatch', + description: 'The time the event was ingested in AWS CloudWatch.', + name: 'awscloudwatch.ingestion_time', + type: 'keyword', + }, + 'netflow.type': { + category: 'netflow', + description: 'The type of NetFlow record described by this event. ', + name: 'netflow.type', + type: 'keyword', + }, + 'netflow.exporter.address': { + category: 'netflow', + description: "Exporter's network address in IP:port format. ", + name: 'netflow.exporter.address', + type: 'keyword', + }, + 'netflow.exporter.source_id': { + category: 'netflow', + description: 'Observation domain ID to which this record belongs. ', + name: 'netflow.exporter.source_id', + type: 'long', + }, + 'netflow.exporter.timestamp': { + category: 'netflow', + description: 'Time and date of export. ', + name: 'netflow.exporter.timestamp', + type: 'date', + }, + 'netflow.exporter.uptime_millis': { + category: 'netflow', + description: 'How long the exporter process has been running, in milliseconds. ', + name: 'netflow.exporter.uptime_millis', + type: 'long', + }, + 'netflow.exporter.version': { + category: 'netflow', + description: 'NetFlow version used. ', + name: 'netflow.exporter.version', + type: 'integer', + }, + 'netflow.octet_delta_count': { + category: 'netflow', + name: 'netflow.octet_delta_count', + type: 'long', + }, + 'netflow.packet_delta_count': { + category: 'netflow', + name: 'netflow.packet_delta_count', + type: 'long', + }, + 'netflow.delta_flow_count': { + category: 'netflow', + name: 'netflow.delta_flow_count', + type: 'long', + }, + 'netflow.protocol_identifier': { + category: 'netflow', + name: 'netflow.protocol_identifier', + type: 'short', + }, + 'netflow.ip_class_of_service': { + category: 'netflow', + name: 'netflow.ip_class_of_service', + type: 'short', + }, + 'netflow.tcp_control_bits': { + category: 'netflow', + name: 'netflow.tcp_control_bits', + type: 'integer', + }, + 'netflow.source_transport_port': { + category: 'netflow', + name: 'netflow.source_transport_port', + type: 'integer', + }, + 'netflow.source_ipv4_address': { + category: 'netflow', + name: 'netflow.source_ipv4_address', + type: 'ip', + }, + 'netflow.source_ipv4_prefix_length': { + category: 'netflow', + name: 'netflow.source_ipv4_prefix_length', + type: 'short', + }, + 'netflow.ingress_interface': { + category: 'netflow', + name: 'netflow.ingress_interface', + type: 'long', + }, + 'netflow.destination_transport_port': { + category: 'netflow', + name: 'netflow.destination_transport_port', + type: 'integer', + }, + 'netflow.destination_ipv4_address': { + category: 'netflow', + name: 'netflow.destination_ipv4_address', + type: 'ip', + }, + 'netflow.destination_ipv4_prefix_length': { + category: 'netflow', + name: 'netflow.destination_ipv4_prefix_length', + type: 'short', + }, + 'netflow.egress_interface': { + category: 'netflow', + name: 'netflow.egress_interface', + type: 'long', + }, + 'netflow.ip_next_hop_ipv4_address': { + category: 'netflow', + name: 'netflow.ip_next_hop_ipv4_address', + type: 'ip', + }, + 'netflow.bgp_source_as_number': { + category: 'netflow', + name: 'netflow.bgp_source_as_number', + type: 'long', + }, + 'netflow.bgp_destination_as_number': { + category: 'netflow', + name: 'netflow.bgp_destination_as_number', + type: 'long', + }, + 'netflow.bgp_next_hop_ipv4_address': { + category: 'netflow', + name: 'netflow.bgp_next_hop_ipv4_address', + type: 'ip', + }, + 'netflow.post_mcast_packet_delta_count': { + category: 'netflow', + name: 'netflow.post_mcast_packet_delta_count', + type: 'long', + }, + 'netflow.post_mcast_octet_delta_count': { + category: 'netflow', + name: 'netflow.post_mcast_octet_delta_count', + type: 'long', + }, + 'netflow.flow_end_sys_up_time': { + category: 'netflow', + name: 'netflow.flow_end_sys_up_time', + type: 'long', + }, + 'netflow.flow_start_sys_up_time': { + category: 'netflow', + name: 'netflow.flow_start_sys_up_time', + type: 'long', + }, + 'netflow.post_octet_delta_count': { + category: 'netflow', + name: 'netflow.post_octet_delta_count', + type: 'long', + }, + 'netflow.post_packet_delta_count': { + category: 'netflow', + name: 'netflow.post_packet_delta_count', + type: 'long', + }, + 'netflow.minimum_ip_total_length': { + category: 'netflow', + name: 'netflow.minimum_ip_total_length', + type: 'long', + }, + 'netflow.maximum_ip_total_length': { + category: 'netflow', + name: 'netflow.maximum_ip_total_length', + type: 'long', + }, + 'netflow.source_ipv6_address': { + category: 'netflow', + name: 'netflow.source_ipv6_address', + type: 'ip', + }, + 'netflow.destination_ipv6_address': { + category: 'netflow', + name: 'netflow.destination_ipv6_address', + type: 'ip', + }, + 'netflow.source_ipv6_prefix_length': { + category: 'netflow', + name: 'netflow.source_ipv6_prefix_length', + type: 'short', + }, + 'netflow.destination_ipv6_prefix_length': { + category: 'netflow', + name: 'netflow.destination_ipv6_prefix_length', + type: 'short', + }, + 'netflow.flow_label_ipv6': { + category: 'netflow', + name: 'netflow.flow_label_ipv6', + type: 'long', + }, + 'netflow.icmp_type_code_ipv4': { + category: 'netflow', + name: 'netflow.icmp_type_code_ipv4', + type: 'integer', + }, + 'netflow.igmp_type': { + category: 'netflow', + name: 'netflow.igmp_type', + type: 'short', + }, + 'netflow.sampling_interval': { + category: 'netflow', + name: 'netflow.sampling_interval', + type: 'long', + }, + 'netflow.sampling_algorithm': { + category: 'netflow', + name: 'netflow.sampling_algorithm', + type: 'short', + }, + 'netflow.flow_active_timeout': { + category: 'netflow', + name: 'netflow.flow_active_timeout', + type: 'integer', + }, + 'netflow.flow_idle_timeout': { + category: 'netflow', + name: 'netflow.flow_idle_timeout', + type: 'integer', + }, + 'netflow.engine_type': { + category: 'netflow', + name: 'netflow.engine_type', + type: 'short', + }, + 'netflow.engine_id': { + category: 'netflow', + name: 'netflow.engine_id', + type: 'short', + }, + 'netflow.exported_octet_total_count': { + category: 'netflow', + name: 'netflow.exported_octet_total_count', + type: 'long', + }, + 'netflow.exported_message_total_count': { + category: 'netflow', + name: 'netflow.exported_message_total_count', + type: 'long', + }, + 'netflow.exported_flow_record_total_count': { + category: 'netflow', + name: 'netflow.exported_flow_record_total_count', + type: 'long', + }, + 'netflow.ipv4_router_sc': { + category: 'netflow', + name: 'netflow.ipv4_router_sc', + type: 'ip', + }, + 'netflow.source_ipv4_prefix': { + category: 'netflow', + name: 'netflow.source_ipv4_prefix', + type: 'ip', + }, + 'netflow.destination_ipv4_prefix': { + category: 'netflow', + name: 'netflow.destination_ipv4_prefix', + type: 'ip', + }, + 'netflow.mpls_top_label_type': { + category: 'netflow', + name: 'netflow.mpls_top_label_type', + type: 'short', + }, + 'netflow.mpls_top_label_ipv4_address': { + category: 'netflow', + name: 'netflow.mpls_top_label_ipv4_address', + type: 'ip', + }, + 'netflow.sampler_id': { + category: 'netflow', + name: 'netflow.sampler_id', + type: 'short', + }, + 'netflow.sampler_mode': { + category: 'netflow', + name: 'netflow.sampler_mode', + type: 'short', + }, + 'netflow.sampler_random_interval': { + category: 'netflow', + name: 'netflow.sampler_random_interval', + type: 'long', + }, + 'netflow.class_id': { + category: 'netflow', + name: 'netflow.class_id', + type: 'long', + }, + 'netflow.minimum_ttl': { + category: 'netflow', + name: 'netflow.minimum_ttl', + type: 'short', + }, + 'netflow.maximum_ttl': { + category: 'netflow', + name: 'netflow.maximum_ttl', + type: 'short', + }, + 'netflow.fragment_identification': { + category: 'netflow', + name: 'netflow.fragment_identification', + type: 'long', + }, + 'netflow.post_ip_class_of_service': { + category: 'netflow', + name: 'netflow.post_ip_class_of_service', + type: 'short', + }, + 'netflow.source_mac_address': { + category: 'netflow', + name: 'netflow.source_mac_address', + type: 'keyword', + }, + 'netflow.post_destination_mac_address': { + category: 'netflow', + name: 'netflow.post_destination_mac_address', + type: 'keyword', + }, + 'netflow.vlan_id': { + category: 'netflow', + name: 'netflow.vlan_id', + type: 'integer', + }, + 'netflow.post_vlan_id': { + category: 'netflow', + name: 'netflow.post_vlan_id', + type: 'integer', + }, + 'netflow.ip_version': { + category: 'netflow', + name: 'netflow.ip_version', + type: 'short', + }, + 'netflow.flow_direction': { + category: 'netflow', + name: 'netflow.flow_direction', + type: 'short', + }, + 'netflow.ip_next_hop_ipv6_address': { + category: 'netflow', + name: 'netflow.ip_next_hop_ipv6_address', + type: 'ip', + }, + 'netflow.bgp_next_hop_ipv6_address': { + category: 'netflow', + name: 'netflow.bgp_next_hop_ipv6_address', + type: 'ip', + }, + 'netflow.ipv6_extension_headers': { + category: 'netflow', + name: 'netflow.ipv6_extension_headers', + type: 'long', + }, + 'netflow.mpls_top_label_stack_section': { + category: 'netflow', + name: 'netflow.mpls_top_label_stack_section', + type: 'short', + }, + 'netflow.mpls_label_stack_section2': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section2', + type: 'short', + }, + 'netflow.mpls_label_stack_section3': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section3', + type: 'short', + }, + 'netflow.mpls_label_stack_section4': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section4', + type: 'short', + }, + 'netflow.mpls_label_stack_section5': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section5', + type: 'short', + }, + 'netflow.mpls_label_stack_section6': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section6', + type: 'short', + }, + 'netflow.mpls_label_stack_section7': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section7', + type: 'short', + }, + 'netflow.mpls_label_stack_section8': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section8', + type: 'short', + }, + 'netflow.mpls_label_stack_section9': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section9', + type: 'short', + }, + 'netflow.mpls_label_stack_section10': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section10', + type: 'short', + }, + 'netflow.destination_mac_address': { + category: 'netflow', + name: 'netflow.destination_mac_address', + type: 'keyword', + }, + 'netflow.post_source_mac_address': { + category: 'netflow', + name: 'netflow.post_source_mac_address', + type: 'keyword', + }, + 'netflow.interface_name': { + category: 'netflow', + name: 'netflow.interface_name', + type: 'keyword', + }, + 'netflow.interface_description': { + category: 'netflow', + name: 'netflow.interface_description', + type: 'keyword', + }, + 'netflow.sampler_name': { + category: 'netflow', + name: 'netflow.sampler_name', + type: 'keyword', + }, + 'netflow.octet_total_count': { + category: 'netflow', + name: 'netflow.octet_total_count', + type: 'long', + }, + 'netflow.packet_total_count': { + category: 'netflow', + name: 'netflow.packet_total_count', + type: 'long', + }, + 'netflow.flags_and_sampler_id': { + category: 'netflow', + name: 'netflow.flags_and_sampler_id', + type: 'long', + }, + 'netflow.fragment_offset': { + category: 'netflow', + name: 'netflow.fragment_offset', + type: 'integer', + }, + 'netflow.forwarding_status': { + category: 'netflow', + name: 'netflow.forwarding_status', + type: 'short', + }, + 'netflow.mpls_vpn_route_distinguisher': { + category: 'netflow', + name: 'netflow.mpls_vpn_route_distinguisher', + type: 'short', + }, + 'netflow.mpls_top_label_prefix_length': { + category: 'netflow', + name: 'netflow.mpls_top_label_prefix_length', + type: 'short', + }, + 'netflow.src_traffic_index': { + category: 'netflow', + name: 'netflow.src_traffic_index', + type: 'long', + }, + 'netflow.dst_traffic_index': { + category: 'netflow', + name: 'netflow.dst_traffic_index', + type: 'long', + }, + 'netflow.application_description': { + category: 'netflow', + name: 'netflow.application_description', + type: 'keyword', + }, + 'netflow.application_id': { + category: 'netflow', + name: 'netflow.application_id', + type: 'short', + }, + 'netflow.application_name': { + category: 'netflow', + name: 'netflow.application_name', + type: 'keyword', + }, + 'netflow.post_ip_diff_serv_code_point': { + category: 'netflow', + name: 'netflow.post_ip_diff_serv_code_point', + type: 'short', + }, + 'netflow.multicast_replication_factor': { + category: 'netflow', + name: 'netflow.multicast_replication_factor', + type: 'long', + }, + 'netflow.class_name': { + category: 'netflow', + name: 'netflow.class_name', + type: 'keyword', + }, + 'netflow.classification_engine_id': { + category: 'netflow', + name: 'netflow.classification_engine_id', + type: 'short', + }, + 'netflow.layer2packet_section_offset': { + category: 'netflow', + name: 'netflow.layer2packet_section_offset', + type: 'integer', + }, + 'netflow.layer2packet_section_size': { + category: 'netflow', + name: 'netflow.layer2packet_section_size', + type: 'integer', + }, + 'netflow.layer2packet_section_data': { + category: 'netflow', + name: 'netflow.layer2packet_section_data', + type: 'short', + }, + 'netflow.bgp_next_adjacent_as_number': { + category: 'netflow', + name: 'netflow.bgp_next_adjacent_as_number', + type: 'long', + }, + 'netflow.bgp_prev_adjacent_as_number': { + category: 'netflow', + name: 'netflow.bgp_prev_adjacent_as_number', + type: 'long', + }, + 'netflow.exporter_ipv4_address': { + category: 'netflow', + name: 'netflow.exporter_ipv4_address', + type: 'ip', + }, + 'netflow.exporter_ipv6_address': { + category: 'netflow', + name: 'netflow.exporter_ipv6_address', + type: 'ip', + }, + 'netflow.dropped_octet_delta_count': { + category: 'netflow', + name: 'netflow.dropped_octet_delta_count', + type: 'long', + }, + 'netflow.dropped_packet_delta_count': { + category: 'netflow', + name: 'netflow.dropped_packet_delta_count', + type: 'long', + }, + 'netflow.dropped_octet_total_count': { + category: 'netflow', + name: 'netflow.dropped_octet_total_count', + type: 'long', + }, + 'netflow.dropped_packet_total_count': { + category: 'netflow', + name: 'netflow.dropped_packet_total_count', + type: 'long', + }, + 'netflow.flow_end_reason': { + category: 'netflow', + name: 'netflow.flow_end_reason', + type: 'short', + }, + 'netflow.common_properties_id': { + category: 'netflow', + name: 'netflow.common_properties_id', + type: 'long', + }, + 'netflow.observation_point_id': { + category: 'netflow', + name: 'netflow.observation_point_id', + type: 'long', + }, + 'netflow.icmp_type_code_ipv6': { + category: 'netflow', + name: 'netflow.icmp_type_code_ipv6', + type: 'integer', + }, + 'netflow.mpls_top_label_ipv6_address': { + category: 'netflow', + name: 'netflow.mpls_top_label_ipv6_address', + type: 'ip', + }, + 'netflow.line_card_id': { + category: 'netflow', + name: 'netflow.line_card_id', + type: 'long', + }, + 'netflow.port_id': { + category: 'netflow', + name: 'netflow.port_id', + type: 'long', + }, + 'netflow.metering_process_id': { + category: 'netflow', + name: 'netflow.metering_process_id', + type: 'long', + }, + 'netflow.exporting_process_id': { + category: 'netflow', + name: 'netflow.exporting_process_id', + type: 'long', + }, + 'netflow.template_id': { + category: 'netflow', + name: 'netflow.template_id', + type: 'integer', + }, + 'netflow.wlan_channel_id': { + category: 'netflow', + name: 'netflow.wlan_channel_id', + type: 'short', + }, + 'netflow.wlan_ssid': { + category: 'netflow', + name: 'netflow.wlan_ssid', + type: 'keyword', + }, + 'netflow.flow_id': { + category: 'netflow', + name: 'netflow.flow_id', + type: 'long', + }, + 'netflow.observation_domain_id': { + category: 'netflow', + name: 'netflow.observation_domain_id', + type: 'long', + }, + 'netflow.flow_start_seconds': { + category: 'netflow', + name: 'netflow.flow_start_seconds', + type: 'date', + }, + 'netflow.flow_end_seconds': { + category: 'netflow', + name: 'netflow.flow_end_seconds', + type: 'date', + }, + 'netflow.flow_start_milliseconds': { + category: 'netflow', + name: 'netflow.flow_start_milliseconds', + type: 'date', + }, + 'netflow.flow_end_milliseconds': { + category: 'netflow', + name: 'netflow.flow_end_milliseconds', + type: 'date', + }, + 'netflow.flow_start_microseconds': { + category: 'netflow', + name: 'netflow.flow_start_microseconds', + type: 'date', + }, + 'netflow.flow_end_microseconds': { + category: 'netflow', + name: 'netflow.flow_end_microseconds', + type: 'date', + }, + 'netflow.flow_start_nanoseconds': { + category: 'netflow', + name: 'netflow.flow_start_nanoseconds', + type: 'date', + }, + 'netflow.flow_end_nanoseconds': { + category: 'netflow', + name: 'netflow.flow_end_nanoseconds', + type: 'date', + }, + 'netflow.flow_start_delta_microseconds': { + category: 'netflow', + name: 'netflow.flow_start_delta_microseconds', + type: 'long', + }, + 'netflow.flow_end_delta_microseconds': { + category: 'netflow', + name: 'netflow.flow_end_delta_microseconds', + type: 'long', + }, + 'netflow.system_init_time_milliseconds': { + category: 'netflow', + name: 'netflow.system_init_time_milliseconds', + type: 'date', + }, + 'netflow.flow_duration_milliseconds': { + category: 'netflow', + name: 'netflow.flow_duration_milliseconds', + type: 'long', + }, + 'netflow.flow_duration_microseconds': { + category: 'netflow', + name: 'netflow.flow_duration_microseconds', + type: 'long', + }, + 'netflow.observed_flow_total_count': { + category: 'netflow', + name: 'netflow.observed_flow_total_count', + type: 'long', + }, + 'netflow.ignored_packet_total_count': { + category: 'netflow', + name: 'netflow.ignored_packet_total_count', + type: 'long', + }, + 'netflow.ignored_octet_total_count': { + category: 'netflow', + name: 'netflow.ignored_octet_total_count', + type: 'long', + }, + 'netflow.not_sent_flow_total_count': { + category: 'netflow', + name: 'netflow.not_sent_flow_total_count', + type: 'long', + }, + 'netflow.not_sent_packet_total_count': { + category: 'netflow', + name: 'netflow.not_sent_packet_total_count', + type: 'long', + }, + 'netflow.not_sent_octet_total_count': { + category: 'netflow', + name: 'netflow.not_sent_octet_total_count', + type: 'long', + }, + 'netflow.destination_ipv6_prefix': { + category: 'netflow', + name: 'netflow.destination_ipv6_prefix', + type: 'ip', + }, + 'netflow.source_ipv6_prefix': { + category: 'netflow', + name: 'netflow.source_ipv6_prefix', + type: 'ip', + }, + 'netflow.post_octet_total_count': { + category: 'netflow', + name: 'netflow.post_octet_total_count', + type: 'long', + }, + 'netflow.post_packet_total_count': { + category: 'netflow', + name: 'netflow.post_packet_total_count', + type: 'long', + }, + 'netflow.flow_key_indicator': { + category: 'netflow', + name: 'netflow.flow_key_indicator', + type: 'long', + }, + 'netflow.post_mcast_packet_total_count': { + category: 'netflow', + name: 'netflow.post_mcast_packet_total_count', + type: 'long', + }, + 'netflow.post_mcast_octet_total_count': { + category: 'netflow', + name: 'netflow.post_mcast_octet_total_count', + type: 'long', + }, + 'netflow.icmp_type_ipv4': { + category: 'netflow', + name: 'netflow.icmp_type_ipv4', + type: 'short', + }, + 'netflow.icmp_code_ipv4': { + category: 'netflow', + name: 'netflow.icmp_code_ipv4', + type: 'short', + }, + 'netflow.icmp_type_ipv6': { + category: 'netflow', + name: 'netflow.icmp_type_ipv6', + type: 'short', + }, + 'netflow.icmp_code_ipv6': { + category: 'netflow', + name: 'netflow.icmp_code_ipv6', + type: 'short', + }, + 'netflow.udp_source_port': { + category: 'netflow', + name: 'netflow.udp_source_port', + type: 'integer', + }, + 'netflow.udp_destination_port': { + category: 'netflow', + name: 'netflow.udp_destination_port', + type: 'integer', + }, + 'netflow.tcp_source_port': { + category: 'netflow', + name: 'netflow.tcp_source_port', + type: 'integer', + }, + 'netflow.tcp_destination_port': { + category: 'netflow', + name: 'netflow.tcp_destination_port', + type: 'integer', + }, + 'netflow.tcp_sequence_number': { + category: 'netflow', + name: 'netflow.tcp_sequence_number', + type: 'long', + }, + 'netflow.tcp_acknowledgement_number': { + category: 'netflow', + name: 'netflow.tcp_acknowledgement_number', + type: 'long', + }, + 'netflow.tcp_window_size': { + category: 'netflow', + name: 'netflow.tcp_window_size', + type: 'integer', + }, + 'netflow.tcp_urgent_pointer': { + category: 'netflow', + name: 'netflow.tcp_urgent_pointer', + type: 'integer', + }, + 'netflow.tcp_header_length': { + category: 'netflow', + name: 'netflow.tcp_header_length', + type: 'short', + }, + 'netflow.ip_header_length': { + category: 'netflow', + name: 'netflow.ip_header_length', + type: 'short', + }, + 'netflow.total_length_ipv4': { + category: 'netflow', + name: 'netflow.total_length_ipv4', + type: 'integer', + }, + 'netflow.payload_length_ipv6': { + category: 'netflow', + name: 'netflow.payload_length_ipv6', + type: 'integer', + }, + 'netflow.ip_ttl': { + category: 'netflow', + name: 'netflow.ip_ttl', + type: 'short', + }, + 'netflow.next_header_ipv6': { + category: 'netflow', + name: 'netflow.next_header_ipv6', + type: 'short', + }, + 'netflow.mpls_payload_length': { + category: 'netflow', + name: 'netflow.mpls_payload_length', + type: 'long', + }, + 'netflow.ip_diff_serv_code_point': { + category: 'netflow', + name: 'netflow.ip_diff_serv_code_point', + type: 'short', + }, + 'netflow.ip_precedence': { + category: 'netflow', + name: 'netflow.ip_precedence', + type: 'short', + }, + 'netflow.fragment_flags': { + category: 'netflow', + name: 'netflow.fragment_flags', + type: 'short', + }, + 'netflow.octet_delta_sum_of_squares': { + category: 'netflow', + name: 'netflow.octet_delta_sum_of_squares', + type: 'long', + }, + 'netflow.octet_total_sum_of_squares': { + category: 'netflow', + name: 'netflow.octet_total_sum_of_squares', + type: 'long', + }, + 'netflow.mpls_top_label_ttl': { + category: 'netflow', + name: 'netflow.mpls_top_label_ttl', + type: 'short', + }, + 'netflow.mpls_label_stack_length': { + category: 'netflow', + name: 'netflow.mpls_label_stack_length', + type: 'long', + }, + 'netflow.mpls_label_stack_depth': { + category: 'netflow', + name: 'netflow.mpls_label_stack_depth', + type: 'long', + }, + 'netflow.mpls_top_label_exp': { + category: 'netflow', + name: 'netflow.mpls_top_label_exp', + type: 'short', + }, + 'netflow.ip_payload_length': { + category: 'netflow', + name: 'netflow.ip_payload_length', + type: 'long', + }, + 'netflow.udp_message_length': { + category: 'netflow', + name: 'netflow.udp_message_length', + type: 'integer', + }, + 'netflow.is_multicast': { + category: 'netflow', + name: 'netflow.is_multicast', + type: 'short', + }, + 'netflow.ipv4_ihl': { + category: 'netflow', + name: 'netflow.ipv4_ihl', + type: 'short', + }, + 'netflow.ipv4_options': { + category: 'netflow', + name: 'netflow.ipv4_options', + type: 'long', + }, + 'netflow.tcp_options': { + category: 'netflow', + name: 'netflow.tcp_options', + type: 'long', + }, + 'netflow.padding_octets': { + category: 'netflow', + name: 'netflow.padding_octets', + type: 'short', + }, + 'netflow.collector_ipv4_address': { + category: 'netflow', + name: 'netflow.collector_ipv4_address', + type: 'ip', + }, + 'netflow.collector_ipv6_address': { + category: 'netflow', + name: 'netflow.collector_ipv6_address', + type: 'ip', + }, + 'netflow.export_interface': { + category: 'netflow', + name: 'netflow.export_interface', + type: 'long', + }, + 'netflow.export_protocol_version': { + category: 'netflow', + name: 'netflow.export_protocol_version', + type: 'short', + }, + 'netflow.export_transport_protocol': { + category: 'netflow', + name: 'netflow.export_transport_protocol', + type: 'short', + }, + 'netflow.collector_transport_port': { + category: 'netflow', + name: 'netflow.collector_transport_port', + type: 'integer', + }, + 'netflow.exporter_transport_port': { + category: 'netflow', + name: 'netflow.exporter_transport_port', + type: 'integer', + }, + 'netflow.tcp_syn_total_count': { + category: 'netflow', + name: 'netflow.tcp_syn_total_count', + type: 'long', + }, + 'netflow.tcp_fin_total_count': { + category: 'netflow', + name: 'netflow.tcp_fin_total_count', + type: 'long', + }, + 'netflow.tcp_rst_total_count': { + category: 'netflow', + name: 'netflow.tcp_rst_total_count', + type: 'long', + }, + 'netflow.tcp_psh_total_count': { + category: 'netflow', + name: 'netflow.tcp_psh_total_count', + type: 'long', + }, + 'netflow.tcp_ack_total_count': { + category: 'netflow', + name: 'netflow.tcp_ack_total_count', + type: 'long', + }, + 'netflow.tcp_urg_total_count': { + category: 'netflow', + name: 'netflow.tcp_urg_total_count', + type: 'long', + }, + 'netflow.ip_total_length': { + category: 'netflow', + name: 'netflow.ip_total_length', + type: 'long', + }, + 'netflow.post_nat_source_ipv4_address': { + category: 'netflow', + name: 'netflow.post_nat_source_ipv4_address', + type: 'ip', + }, + 'netflow.post_nat_destination_ipv4_address': { + category: 'netflow', + name: 'netflow.post_nat_destination_ipv4_address', + type: 'ip', + }, + 'netflow.post_napt_source_transport_port': { + category: 'netflow', + name: 'netflow.post_napt_source_transport_port', + type: 'integer', + }, + 'netflow.post_napt_destination_transport_port': { + category: 'netflow', + name: 'netflow.post_napt_destination_transport_port', + type: 'integer', + }, + 'netflow.nat_originating_address_realm': { + category: 'netflow', + name: 'netflow.nat_originating_address_realm', + type: 'short', + }, + 'netflow.nat_event': { + category: 'netflow', + name: 'netflow.nat_event', + type: 'short', + }, + 'netflow.initiator_octets': { + category: 'netflow', + name: 'netflow.initiator_octets', + type: 'long', + }, + 'netflow.responder_octets': { + category: 'netflow', + name: 'netflow.responder_octets', + type: 'long', + }, + 'netflow.firewall_event': { + category: 'netflow', + name: 'netflow.firewall_event', + type: 'short', + }, + 'netflow.ingress_vrfid': { + category: 'netflow', + name: 'netflow.ingress_vrfid', + type: 'long', + }, + 'netflow.egress_vrfid': { + category: 'netflow', + name: 'netflow.egress_vrfid', + type: 'long', + }, + 'netflow.vr_fname': { + category: 'netflow', + name: 'netflow.vr_fname', + type: 'keyword', + }, + 'netflow.post_mpls_top_label_exp': { + category: 'netflow', + name: 'netflow.post_mpls_top_label_exp', + type: 'short', + }, + 'netflow.tcp_window_scale': { + category: 'netflow', + name: 'netflow.tcp_window_scale', + type: 'integer', + }, + 'netflow.biflow_direction': { + category: 'netflow', + name: 'netflow.biflow_direction', + type: 'short', + }, + 'netflow.ethernet_header_length': { + category: 'netflow', + name: 'netflow.ethernet_header_length', + type: 'short', + }, + 'netflow.ethernet_payload_length': { + category: 'netflow', + name: 'netflow.ethernet_payload_length', + type: 'integer', + }, + 'netflow.ethernet_total_length': { + category: 'netflow', + name: 'netflow.ethernet_total_length', + type: 'integer', + }, + 'netflow.dot1q_vlan_id': { + category: 'netflow', + name: 'netflow.dot1q_vlan_id', + type: 'integer', + }, + 'netflow.dot1q_priority': { + category: 'netflow', + name: 'netflow.dot1q_priority', + type: 'short', + }, + 'netflow.dot1q_customer_vlan_id': { + category: 'netflow', + name: 'netflow.dot1q_customer_vlan_id', + type: 'integer', + }, + 'netflow.dot1q_customer_priority': { + category: 'netflow', + name: 'netflow.dot1q_customer_priority', + type: 'short', + }, + 'netflow.metro_evc_id': { + category: 'netflow', + name: 'netflow.metro_evc_id', + type: 'keyword', + }, + 'netflow.metro_evc_type': { + category: 'netflow', + name: 'netflow.metro_evc_type', + type: 'short', + }, + 'netflow.pseudo_wire_id': { + category: 'netflow', + name: 'netflow.pseudo_wire_id', + type: 'long', + }, + 'netflow.pseudo_wire_type': { + category: 'netflow', + name: 'netflow.pseudo_wire_type', + type: 'integer', + }, + 'netflow.pseudo_wire_control_word': { + category: 'netflow', + name: 'netflow.pseudo_wire_control_word', + type: 'long', + }, + 'netflow.ingress_physical_interface': { + category: 'netflow', + name: 'netflow.ingress_physical_interface', + type: 'long', + }, + 'netflow.egress_physical_interface': { + category: 'netflow', + name: 'netflow.egress_physical_interface', + type: 'long', + }, + 'netflow.post_dot1q_vlan_id': { + category: 'netflow', + name: 'netflow.post_dot1q_vlan_id', + type: 'integer', + }, + 'netflow.post_dot1q_customer_vlan_id': { + category: 'netflow', + name: 'netflow.post_dot1q_customer_vlan_id', + type: 'integer', + }, + 'netflow.ethernet_type': { + category: 'netflow', + name: 'netflow.ethernet_type', + type: 'integer', + }, + 'netflow.post_ip_precedence': { + category: 'netflow', + name: 'netflow.post_ip_precedence', + type: 'short', + }, + 'netflow.collection_time_milliseconds': { + category: 'netflow', + name: 'netflow.collection_time_milliseconds', + type: 'date', + }, + 'netflow.export_sctp_stream_id': { + category: 'netflow', + name: 'netflow.export_sctp_stream_id', + type: 'integer', + }, + 'netflow.max_export_seconds': { + category: 'netflow', + name: 'netflow.max_export_seconds', + type: 'date', + }, + 'netflow.max_flow_end_seconds': { + category: 'netflow', + name: 'netflow.max_flow_end_seconds', + type: 'date', + }, + 'netflow.message_md5_checksum': { + category: 'netflow', + name: 'netflow.message_md5_checksum', + type: 'short', + }, + 'netflow.message_scope': { + category: 'netflow', + name: 'netflow.message_scope', + type: 'short', + }, + 'netflow.min_export_seconds': { + category: 'netflow', + name: 'netflow.min_export_seconds', + type: 'date', + }, + 'netflow.min_flow_start_seconds': { + category: 'netflow', + name: 'netflow.min_flow_start_seconds', + type: 'date', + }, + 'netflow.opaque_octets': { + category: 'netflow', + name: 'netflow.opaque_octets', + type: 'short', + }, + 'netflow.session_scope': { + category: 'netflow', + name: 'netflow.session_scope', + type: 'short', + }, + 'netflow.max_flow_end_microseconds': { + category: 'netflow', + name: 'netflow.max_flow_end_microseconds', + type: 'date', + }, + 'netflow.max_flow_end_milliseconds': { + category: 'netflow', + name: 'netflow.max_flow_end_milliseconds', + type: 'date', + }, + 'netflow.max_flow_end_nanoseconds': { + category: 'netflow', + name: 'netflow.max_flow_end_nanoseconds', + type: 'date', + }, + 'netflow.min_flow_start_microseconds': { + category: 'netflow', + name: 'netflow.min_flow_start_microseconds', + type: 'date', + }, + 'netflow.min_flow_start_milliseconds': { + category: 'netflow', + name: 'netflow.min_flow_start_milliseconds', + type: 'date', + }, + 'netflow.min_flow_start_nanoseconds': { + category: 'netflow', + name: 'netflow.min_flow_start_nanoseconds', + type: 'date', + }, + 'netflow.collector_certificate': { + category: 'netflow', + name: 'netflow.collector_certificate', + type: 'short', + }, + 'netflow.exporter_certificate': { + category: 'netflow', + name: 'netflow.exporter_certificate', + type: 'short', + }, + 'netflow.data_records_reliability': { + category: 'netflow', + name: 'netflow.data_records_reliability', + type: 'boolean', + }, + 'netflow.observation_point_type': { + category: 'netflow', + name: 'netflow.observation_point_type', + type: 'short', + }, + 'netflow.new_connection_delta_count': { + category: 'netflow', + name: 'netflow.new_connection_delta_count', + type: 'long', + }, + 'netflow.connection_sum_duration_seconds': { + category: 'netflow', + name: 'netflow.connection_sum_duration_seconds', + type: 'long', + }, + 'netflow.connection_transaction_id': { + category: 'netflow', + name: 'netflow.connection_transaction_id', + type: 'long', + }, + 'netflow.post_nat_source_ipv6_address': { + category: 'netflow', + name: 'netflow.post_nat_source_ipv6_address', + type: 'ip', + }, + 'netflow.post_nat_destination_ipv6_address': { + category: 'netflow', + name: 'netflow.post_nat_destination_ipv6_address', + type: 'ip', + }, + 'netflow.nat_pool_id': { + category: 'netflow', + name: 'netflow.nat_pool_id', + type: 'long', + }, + 'netflow.nat_pool_name': { + category: 'netflow', + name: 'netflow.nat_pool_name', + type: 'keyword', + }, + 'netflow.anonymization_flags': { + category: 'netflow', + name: 'netflow.anonymization_flags', + type: 'integer', + }, + 'netflow.anonymization_technique': { + category: 'netflow', + name: 'netflow.anonymization_technique', + type: 'integer', + }, + 'netflow.information_element_index': { + category: 'netflow', + name: 'netflow.information_element_index', + type: 'integer', + }, + 'netflow.p2p_technology': { + category: 'netflow', + name: 'netflow.p2p_technology', + type: 'keyword', + }, + 'netflow.tunnel_technology': { + category: 'netflow', + name: 'netflow.tunnel_technology', + type: 'keyword', + }, + 'netflow.encrypted_technology': { + category: 'netflow', + name: 'netflow.encrypted_technology', + type: 'keyword', + }, + 'netflow.bgp_validity_state': { + category: 'netflow', + name: 'netflow.bgp_validity_state', + type: 'short', + }, + 'netflow.ip_sec_spi': { + category: 'netflow', + name: 'netflow.ip_sec_spi', + type: 'long', + }, + 'netflow.gre_key': { + category: 'netflow', + name: 'netflow.gre_key', + type: 'long', + }, + 'netflow.nat_type': { + category: 'netflow', + name: 'netflow.nat_type', + type: 'short', + }, + 'netflow.initiator_packets': { + category: 'netflow', + name: 'netflow.initiator_packets', + type: 'long', + }, + 'netflow.responder_packets': { + category: 'netflow', + name: 'netflow.responder_packets', + type: 'long', + }, + 'netflow.observation_domain_name': { + category: 'netflow', + name: 'netflow.observation_domain_name', + type: 'keyword', + }, + 'netflow.selection_sequence_id': { + category: 'netflow', + name: 'netflow.selection_sequence_id', + type: 'long', + }, + 'netflow.selector_id': { + category: 'netflow', + name: 'netflow.selector_id', + type: 'long', + }, + 'netflow.information_element_id': { + category: 'netflow', + name: 'netflow.information_element_id', + type: 'integer', + }, + 'netflow.selector_algorithm': { + category: 'netflow', + name: 'netflow.selector_algorithm', + type: 'integer', + }, + 'netflow.sampling_packet_interval': { + category: 'netflow', + name: 'netflow.sampling_packet_interval', + type: 'long', + }, + 'netflow.sampling_packet_space': { + category: 'netflow', + name: 'netflow.sampling_packet_space', + type: 'long', + }, + 'netflow.sampling_time_interval': { + category: 'netflow', + name: 'netflow.sampling_time_interval', + type: 'long', + }, + 'netflow.sampling_time_space': { + category: 'netflow', + name: 'netflow.sampling_time_space', + type: 'long', + }, + 'netflow.sampling_size': { + category: 'netflow', + name: 'netflow.sampling_size', + type: 'long', + }, + 'netflow.sampling_population': { + category: 'netflow', + name: 'netflow.sampling_population', + type: 'long', + }, + 'netflow.sampling_probability': { + category: 'netflow', + name: 'netflow.sampling_probability', + type: 'double', + }, + 'netflow.data_link_frame_size': { + category: 'netflow', + name: 'netflow.data_link_frame_size', + type: 'integer', + }, + 'netflow.ip_header_packet_section': { + category: 'netflow', + name: 'netflow.ip_header_packet_section', + type: 'short', + }, + 'netflow.ip_payload_packet_section': { + category: 'netflow', + name: 'netflow.ip_payload_packet_section', + type: 'short', + }, + 'netflow.data_link_frame_section': { + category: 'netflow', + name: 'netflow.data_link_frame_section', + type: 'short', + }, + 'netflow.mpls_label_stack_section': { + category: 'netflow', + name: 'netflow.mpls_label_stack_section', + type: 'short', + }, + 'netflow.mpls_payload_packet_section': { + category: 'netflow', + name: 'netflow.mpls_payload_packet_section', + type: 'short', + }, + 'netflow.selector_id_total_pkts_observed': { + category: 'netflow', + name: 'netflow.selector_id_total_pkts_observed', + type: 'long', + }, + 'netflow.selector_id_total_pkts_selected': { + category: 'netflow', + name: 'netflow.selector_id_total_pkts_selected', + type: 'long', + }, + 'netflow.absolute_error': { + category: 'netflow', + name: 'netflow.absolute_error', + type: 'double', + }, + 'netflow.relative_error': { + category: 'netflow', + name: 'netflow.relative_error', + type: 'double', + }, + 'netflow.observation_time_seconds': { + category: 'netflow', + name: 'netflow.observation_time_seconds', + type: 'date', + }, + 'netflow.observation_time_milliseconds': { + category: 'netflow', + name: 'netflow.observation_time_milliseconds', + type: 'date', + }, + 'netflow.observation_time_microseconds': { + category: 'netflow', + name: 'netflow.observation_time_microseconds', + type: 'date', + }, + 'netflow.observation_time_nanoseconds': { + category: 'netflow', + name: 'netflow.observation_time_nanoseconds', + type: 'date', + }, + 'netflow.digest_hash_value': { + category: 'netflow', + name: 'netflow.digest_hash_value', + type: 'long', + }, + 'netflow.hash_ip_payload_offset': { + category: 'netflow', + name: 'netflow.hash_ip_payload_offset', + type: 'long', + }, + 'netflow.hash_ip_payload_size': { + category: 'netflow', + name: 'netflow.hash_ip_payload_size', + type: 'long', + }, + 'netflow.hash_output_range_min': { + category: 'netflow', + name: 'netflow.hash_output_range_min', + type: 'long', + }, + 'netflow.hash_output_range_max': { + category: 'netflow', + name: 'netflow.hash_output_range_max', + type: 'long', + }, + 'netflow.hash_selected_range_min': { + category: 'netflow', + name: 'netflow.hash_selected_range_min', + type: 'long', + }, + 'netflow.hash_selected_range_max': { + category: 'netflow', + name: 'netflow.hash_selected_range_max', + type: 'long', + }, + 'netflow.hash_digest_output': { + category: 'netflow', + name: 'netflow.hash_digest_output', + type: 'boolean', + }, + 'netflow.hash_initialiser_value': { + category: 'netflow', + name: 'netflow.hash_initialiser_value', + type: 'long', + }, + 'netflow.selector_name': { + category: 'netflow', + name: 'netflow.selector_name', + type: 'keyword', + }, + 'netflow.upper_ci_limit': { + category: 'netflow', + name: 'netflow.upper_ci_limit', + type: 'double', + }, + 'netflow.lower_ci_limit': { + category: 'netflow', + name: 'netflow.lower_ci_limit', + type: 'double', + }, + 'netflow.confidence_level': { + category: 'netflow', + name: 'netflow.confidence_level', + type: 'double', + }, + 'netflow.information_element_data_type': { + category: 'netflow', + name: 'netflow.information_element_data_type', + type: 'short', + }, + 'netflow.information_element_description': { + category: 'netflow', + name: 'netflow.information_element_description', + type: 'keyword', + }, + 'netflow.information_element_name': { + category: 'netflow', + name: 'netflow.information_element_name', + type: 'keyword', + }, + 'netflow.information_element_range_begin': { + category: 'netflow', + name: 'netflow.information_element_range_begin', + type: 'long', + }, + 'netflow.information_element_range_end': { + category: 'netflow', + name: 'netflow.information_element_range_end', + type: 'long', + }, + 'netflow.information_element_semantics': { + category: 'netflow', + name: 'netflow.information_element_semantics', + type: 'short', + }, + 'netflow.information_element_units': { + category: 'netflow', + name: 'netflow.information_element_units', + type: 'integer', + }, + 'netflow.private_enterprise_number': { + category: 'netflow', + name: 'netflow.private_enterprise_number', + type: 'long', + }, + 'netflow.virtual_station_interface_id': { + category: 'netflow', + name: 'netflow.virtual_station_interface_id', + type: 'short', + }, + 'netflow.virtual_station_interface_name': { + category: 'netflow', + name: 'netflow.virtual_station_interface_name', + type: 'keyword', + }, + 'netflow.virtual_station_uuid': { + category: 'netflow', + name: 'netflow.virtual_station_uuid', + type: 'short', + }, + 'netflow.virtual_station_name': { + category: 'netflow', + name: 'netflow.virtual_station_name', + type: 'keyword', + }, + 'netflow.layer2_segment_id': { + category: 'netflow', + name: 'netflow.layer2_segment_id', + type: 'long', + }, + 'netflow.layer2_octet_delta_count': { + category: 'netflow', + name: 'netflow.layer2_octet_delta_count', + type: 'long', + }, + 'netflow.layer2_octet_total_count': { + category: 'netflow', + name: 'netflow.layer2_octet_total_count', + type: 'long', + }, + 'netflow.ingress_unicast_packet_total_count': { + category: 'netflow', + name: 'netflow.ingress_unicast_packet_total_count', + type: 'long', + }, + 'netflow.ingress_multicast_packet_total_count': { + category: 'netflow', + name: 'netflow.ingress_multicast_packet_total_count', + type: 'long', + }, + 'netflow.ingress_broadcast_packet_total_count': { + category: 'netflow', + name: 'netflow.ingress_broadcast_packet_total_count', + type: 'long', + }, + 'netflow.egress_unicast_packet_total_count': { + category: 'netflow', + name: 'netflow.egress_unicast_packet_total_count', + type: 'long', + }, + 'netflow.egress_broadcast_packet_total_count': { + category: 'netflow', + name: 'netflow.egress_broadcast_packet_total_count', + type: 'long', + }, + 'netflow.monitoring_interval_start_milli_seconds': { + category: 'netflow', + name: 'netflow.monitoring_interval_start_milli_seconds', + type: 'date', + }, + 'netflow.monitoring_interval_end_milli_seconds': { + category: 'netflow', + name: 'netflow.monitoring_interval_end_milli_seconds', + type: 'date', + }, + 'netflow.port_range_start': { + category: 'netflow', + name: 'netflow.port_range_start', + type: 'integer', + }, + 'netflow.port_range_end': { + category: 'netflow', + name: 'netflow.port_range_end', + type: 'integer', + }, + 'netflow.port_range_step_size': { + category: 'netflow', + name: 'netflow.port_range_step_size', + type: 'integer', + }, + 'netflow.port_range_num_ports': { + category: 'netflow', + name: 'netflow.port_range_num_ports', + type: 'integer', + }, + 'netflow.sta_mac_address': { + category: 'netflow', + name: 'netflow.sta_mac_address', + type: 'keyword', + }, + 'netflow.sta_ipv4_address': { + category: 'netflow', + name: 'netflow.sta_ipv4_address', + type: 'ip', + }, + 'netflow.wtp_mac_address': { + category: 'netflow', + name: 'netflow.wtp_mac_address', + type: 'keyword', + }, + 'netflow.ingress_interface_type': { + category: 'netflow', + name: 'netflow.ingress_interface_type', + type: 'long', + }, + 'netflow.egress_interface_type': { + category: 'netflow', + name: 'netflow.egress_interface_type', + type: 'long', + }, + 'netflow.rtp_sequence_number': { + category: 'netflow', + name: 'netflow.rtp_sequence_number', + type: 'integer', + }, + 'netflow.user_name': { + category: 'netflow', + name: 'netflow.user_name', + type: 'keyword', + }, + 'netflow.application_category_name': { + category: 'netflow', + name: 'netflow.application_category_name', + type: 'keyword', + }, + 'netflow.application_sub_category_name': { + category: 'netflow', + name: 'netflow.application_sub_category_name', + type: 'keyword', + }, + 'netflow.application_group_name': { + category: 'netflow', + name: 'netflow.application_group_name', + type: 'keyword', + }, + 'netflow.original_flows_present': { + category: 'netflow', + name: 'netflow.original_flows_present', + type: 'long', + }, + 'netflow.original_flows_initiated': { + category: 'netflow', + name: 'netflow.original_flows_initiated', + type: 'long', + }, + 'netflow.original_flows_completed': { + category: 'netflow', + name: 'netflow.original_flows_completed', + type: 'long', + }, + 'netflow.distinct_count_of_source_ip_address': { + category: 'netflow', + name: 'netflow.distinct_count_of_source_ip_address', + type: 'long', + }, + 'netflow.distinct_count_of_destination_ip_address': { + category: 'netflow', + name: 'netflow.distinct_count_of_destination_ip_address', + type: 'long', + }, + 'netflow.distinct_count_of_source_ipv4_address': { + category: 'netflow', + name: 'netflow.distinct_count_of_source_ipv4_address', + type: 'long', + }, + 'netflow.distinct_count_of_destination_ipv4_address': { + category: 'netflow', + name: 'netflow.distinct_count_of_destination_ipv4_address', + type: 'long', + }, + 'netflow.distinct_count_of_source_ipv6_address': { + category: 'netflow', + name: 'netflow.distinct_count_of_source_ipv6_address', + type: 'long', + }, + 'netflow.distinct_count_of_destination_ipv6_address': { + category: 'netflow', + name: 'netflow.distinct_count_of_destination_ipv6_address', + type: 'long', + }, + 'netflow.value_distribution_method': { + category: 'netflow', + name: 'netflow.value_distribution_method', + type: 'short', + }, + 'netflow.rfc3550_jitter_milliseconds': { + category: 'netflow', + name: 'netflow.rfc3550_jitter_milliseconds', + type: 'long', + }, + 'netflow.rfc3550_jitter_microseconds': { + category: 'netflow', + name: 'netflow.rfc3550_jitter_microseconds', + type: 'long', + }, + 'netflow.rfc3550_jitter_nanoseconds': { + category: 'netflow', + name: 'netflow.rfc3550_jitter_nanoseconds', + type: 'long', + }, + 'netflow.dot1q_dei': { + category: 'netflow', + name: 'netflow.dot1q_dei', + type: 'boolean', + }, + 'netflow.dot1q_customer_dei': { + category: 'netflow', + name: 'netflow.dot1q_customer_dei', + type: 'boolean', + }, + 'netflow.flow_selector_algorithm': { + category: 'netflow', + name: 'netflow.flow_selector_algorithm', + type: 'integer', + }, + 'netflow.flow_selected_octet_delta_count': { + category: 'netflow', + name: 'netflow.flow_selected_octet_delta_count', + type: 'long', + }, + 'netflow.flow_selected_packet_delta_count': { + category: 'netflow', + name: 'netflow.flow_selected_packet_delta_count', + type: 'long', + }, + 'netflow.flow_selected_flow_delta_count': { + category: 'netflow', + name: 'netflow.flow_selected_flow_delta_count', + type: 'long', + }, + 'netflow.selector_id_total_flows_observed': { + category: 'netflow', + name: 'netflow.selector_id_total_flows_observed', + type: 'long', + }, + 'netflow.selector_id_total_flows_selected': { + category: 'netflow', + name: 'netflow.selector_id_total_flows_selected', + type: 'long', + }, + 'netflow.sampling_flow_interval': { + category: 'netflow', + name: 'netflow.sampling_flow_interval', + type: 'long', + }, + 'netflow.sampling_flow_spacing': { + category: 'netflow', + name: 'netflow.sampling_flow_spacing', + type: 'long', + }, + 'netflow.flow_sampling_time_interval': { + category: 'netflow', + name: 'netflow.flow_sampling_time_interval', + type: 'long', + }, + 'netflow.flow_sampling_time_spacing': { + category: 'netflow', + name: 'netflow.flow_sampling_time_spacing', + type: 'long', + }, + 'netflow.hash_flow_domain': { + category: 'netflow', + name: 'netflow.hash_flow_domain', + type: 'integer', + }, + 'netflow.transport_octet_delta_count': { + category: 'netflow', + name: 'netflow.transport_octet_delta_count', + type: 'long', + }, + 'netflow.transport_packet_delta_count': { + category: 'netflow', + name: 'netflow.transport_packet_delta_count', + type: 'long', + }, + 'netflow.original_exporter_ipv4_address': { + category: 'netflow', + name: 'netflow.original_exporter_ipv4_address', + type: 'ip', + }, + 'netflow.original_exporter_ipv6_address': { + category: 'netflow', + name: 'netflow.original_exporter_ipv6_address', + type: 'ip', + }, + 'netflow.original_observation_domain_id': { + category: 'netflow', + name: 'netflow.original_observation_domain_id', + type: 'long', + }, + 'netflow.intermediate_process_id': { + category: 'netflow', + name: 'netflow.intermediate_process_id', + type: 'long', + }, + 'netflow.ignored_data_record_total_count': { + category: 'netflow', + name: 'netflow.ignored_data_record_total_count', + type: 'long', + }, + 'netflow.data_link_frame_type': { + category: 'netflow', + name: 'netflow.data_link_frame_type', + type: 'integer', + }, + 'netflow.section_offset': { + category: 'netflow', + name: 'netflow.section_offset', + type: 'integer', + }, + 'netflow.section_exported_octets': { + category: 'netflow', + name: 'netflow.section_exported_octets', + type: 'integer', + }, + 'netflow.dot1q_service_instance_tag': { + category: 'netflow', + name: 'netflow.dot1q_service_instance_tag', + type: 'short', + }, + 'netflow.dot1q_service_instance_id': { + category: 'netflow', + name: 'netflow.dot1q_service_instance_id', + type: 'long', + }, + 'netflow.dot1q_service_instance_priority': { + category: 'netflow', + name: 'netflow.dot1q_service_instance_priority', + type: 'short', + }, + 'netflow.dot1q_customer_source_mac_address': { + category: 'netflow', + name: 'netflow.dot1q_customer_source_mac_address', + type: 'keyword', + }, + 'netflow.dot1q_customer_destination_mac_address': { + category: 'netflow', + name: 'netflow.dot1q_customer_destination_mac_address', + type: 'keyword', + }, + 'netflow.post_layer2_octet_delta_count': { + category: 'netflow', + name: 'netflow.post_layer2_octet_delta_count', + type: 'long', + }, + 'netflow.post_mcast_layer2_octet_delta_count': { + category: 'netflow', + name: 'netflow.post_mcast_layer2_octet_delta_count', + type: 'long', + }, + 'netflow.post_layer2_octet_total_count': { + category: 'netflow', + name: 'netflow.post_layer2_octet_total_count', + type: 'long', + }, + 'netflow.post_mcast_layer2_octet_total_count': { + category: 'netflow', + name: 'netflow.post_mcast_layer2_octet_total_count', + type: 'long', + }, + 'netflow.minimum_layer2_total_length': { + category: 'netflow', + name: 'netflow.minimum_layer2_total_length', + type: 'long', + }, + 'netflow.maximum_layer2_total_length': { + category: 'netflow', + name: 'netflow.maximum_layer2_total_length', + type: 'long', + }, + 'netflow.dropped_layer2_octet_delta_count': { + category: 'netflow', + name: 'netflow.dropped_layer2_octet_delta_count', + type: 'long', + }, + 'netflow.dropped_layer2_octet_total_count': { + category: 'netflow', + name: 'netflow.dropped_layer2_octet_total_count', + type: 'long', + }, + 'netflow.ignored_layer2_octet_total_count': { + category: 'netflow', + name: 'netflow.ignored_layer2_octet_total_count', + type: 'long', + }, + 'netflow.not_sent_layer2_octet_total_count': { + category: 'netflow', + name: 'netflow.not_sent_layer2_octet_total_count', + type: 'long', + }, + 'netflow.layer2_octet_delta_sum_of_squares': { + category: 'netflow', + name: 'netflow.layer2_octet_delta_sum_of_squares', + type: 'long', + }, + 'netflow.layer2_octet_total_sum_of_squares': { + category: 'netflow', + name: 'netflow.layer2_octet_total_sum_of_squares', + type: 'long', + }, + 'netflow.layer2_frame_delta_count': { + category: 'netflow', + name: 'netflow.layer2_frame_delta_count', + type: 'long', + }, + 'netflow.layer2_frame_total_count': { + category: 'netflow', + name: 'netflow.layer2_frame_total_count', + type: 'long', + }, + 'netflow.pseudo_wire_destination_ipv4_address': { + category: 'netflow', + name: 'netflow.pseudo_wire_destination_ipv4_address', + type: 'ip', + }, + 'netflow.ignored_layer2_frame_total_count': { + category: 'netflow', + name: 'netflow.ignored_layer2_frame_total_count', + type: 'long', + }, + 'netflow.mib_object_value_integer': { + category: 'netflow', + name: 'netflow.mib_object_value_integer', + type: 'integer', + }, + 'netflow.mib_object_value_octet_string': { + category: 'netflow', + name: 'netflow.mib_object_value_octet_string', + type: 'short', + }, + 'netflow.mib_object_value_oid': { + category: 'netflow', + name: 'netflow.mib_object_value_oid', + type: 'short', + }, + 'netflow.mib_object_value_bits': { + category: 'netflow', + name: 'netflow.mib_object_value_bits', + type: 'short', + }, + 'netflow.mib_object_value_ip_address': { + category: 'netflow', + name: 'netflow.mib_object_value_ip_address', + type: 'ip', + }, + 'netflow.mib_object_value_counter': { + category: 'netflow', + name: 'netflow.mib_object_value_counter', + type: 'long', + }, + 'netflow.mib_object_value_gauge': { + category: 'netflow', + name: 'netflow.mib_object_value_gauge', + type: 'long', + }, + 'netflow.mib_object_value_time_ticks': { + category: 'netflow', + name: 'netflow.mib_object_value_time_ticks', + type: 'long', + }, + 'netflow.mib_object_value_unsigned': { + category: 'netflow', + name: 'netflow.mib_object_value_unsigned', + type: 'long', + }, + 'netflow.mib_object_identifier': { + category: 'netflow', + name: 'netflow.mib_object_identifier', + type: 'short', + }, + 'netflow.mib_sub_identifier': { + category: 'netflow', + name: 'netflow.mib_sub_identifier', + type: 'long', + }, + 'netflow.mib_index_indicator': { + category: 'netflow', + name: 'netflow.mib_index_indicator', + type: 'long', + }, + 'netflow.mib_capture_time_semantics': { + category: 'netflow', + name: 'netflow.mib_capture_time_semantics', + type: 'short', + }, + 'netflow.mib_context_engine_id': { + category: 'netflow', + name: 'netflow.mib_context_engine_id', + type: 'short', + }, + 'netflow.mib_context_name': { + category: 'netflow', + name: 'netflow.mib_context_name', + type: 'keyword', + }, + 'netflow.mib_object_name': { + category: 'netflow', + name: 'netflow.mib_object_name', + type: 'keyword', + }, + 'netflow.mib_object_description': { + category: 'netflow', + name: 'netflow.mib_object_description', + type: 'keyword', + }, + 'netflow.mib_object_syntax': { + category: 'netflow', + name: 'netflow.mib_object_syntax', + type: 'keyword', + }, + 'netflow.mib_module_name': { + category: 'netflow', + name: 'netflow.mib_module_name', + type: 'keyword', + }, + 'netflow.mobile_imsi': { + category: 'netflow', + name: 'netflow.mobile_imsi', + type: 'keyword', + }, + 'netflow.mobile_msisdn': { + category: 'netflow', + name: 'netflow.mobile_msisdn', + type: 'keyword', + }, + 'netflow.http_status_code': { + category: 'netflow', + name: 'netflow.http_status_code', + type: 'integer', + }, + 'netflow.source_transport_ports_limit': { + category: 'netflow', + name: 'netflow.source_transport_ports_limit', + type: 'integer', + }, + 'netflow.http_request_method': { + category: 'netflow', + name: 'netflow.http_request_method', + type: 'keyword', + }, + 'netflow.http_request_host': { + category: 'netflow', + name: 'netflow.http_request_host', + type: 'keyword', + }, + 'netflow.http_request_target': { + category: 'netflow', + name: 'netflow.http_request_target', + type: 'keyword', + }, + 'netflow.http_message_version': { + category: 'netflow', + name: 'netflow.http_message_version', + type: 'keyword', + }, + 'netflow.nat_instance_id': { + category: 'netflow', + name: 'netflow.nat_instance_id', + type: 'long', + }, + 'netflow.internal_address_realm': { + category: 'netflow', + name: 'netflow.internal_address_realm', + type: 'short', + }, + 'netflow.external_address_realm': { + category: 'netflow', + name: 'netflow.external_address_realm', + type: 'short', + }, + 'netflow.nat_quota_exceeded_event': { + category: 'netflow', + name: 'netflow.nat_quota_exceeded_event', + type: 'long', + }, + 'netflow.nat_threshold_event': { + category: 'netflow', + name: 'netflow.nat_threshold_event', + type: 'long', + }, + 'netflow.http_user_agent': { + category: 'netflow', + name: 'netflow.http_user_agent', + type: 'keyword', + }, + 'netflow.http_content_type': { + category: 'netflow', + name: 'netflow.http_content_type', + type: 'keyword', + }, + 'netflow.http_reason_phrase': { + category: 'netflow', + name: 'netflow.http_reason_phrase', + type: 'keyword', + }, + 'netflow.max_session_entries': { + category: 'netflow', + name: 'netflow.max_session_entries', + type: 'long', + }, + 'netflow.max_bib_entries': { + category: 'netflow', + name: 'netflow.max_bib_entries', + type: 'long', + }, + 'netflow.max_entries_per_user': { + category: 'netflow', + name: 'netflow.max_entries_per_user', + type: 'long', + }, + 'netflow.max_subscribers': { + category: 'netflow', + name: 'netflow.max_subscribers', + type: 'long', + }, + 'netflow.max_fragments_pending_reassembly': { + category: 'netflow', + name: 'netflow.max_fragments_pending_reassembly', + type: 'long', + }, + 'netflow.address_pool_high_threshold': { + category: 'netflow', + name: 'netflow.address_pool_high_threshold', + type: 'long', + }, + 'netflow.address_pool_low_threshold': { + category: 'netflow', + name: 'netflow.address_pool_low_threshold', + type: 'long', + }, + 'netflow.address_port_mapping_high_threshold': { + category: 'netflow', + name: 'netflow.address_port_mapping_high_threshold', + type: 'long', + }, + 'netflow.address_port_mapping_low_threshold': { + category: 'netflow', + name: 'netflow.address_port_mapping_low_threshold', + type: 'long', + }, + 'netflow.address_port_mapping_per_user_high_threshold': { + category: 'netflow', + name: 'netflow.address_port_mapping_per_user_high_threshold', + type: 'long', + }, + 'netflow.global_address_mapping_high_threshold': { + category: 'netflow', + name: 'netflow.global_address_mapping_high_threshold', + type: 'long', + }, + 'netflow.vpn_identifier': { + category: 'netflow', + name: 'netflow.vpn_identifier', + type: 'short', + }, + bucket_name: { + category: 'base', + description: 'Name of the S3 bucket that this log retrieved from. ', + name: 'bucket_name', + type: 'keyword', + }, + object_key: { + category: 'base', + description: 'Name of the S3 object that this log retrieved from. ', + name: 'object_key', + type: 'keyword', + }, + 'cef.version': { + category: 'cef', + description: 'Version of the CEF specification used by the message. ', + name: 'cef.version', + type: 'keyword', + }, + 'cef.device.vendor': { + category: 'cef', + description: 'Vendor of the device that produced the message. ', + name: 'cef.device.vendor', + type: 'keyword', + }, + 'cef.device.product': { + category: 'cef', + description: 'Product of the device that produced the message. ', + name: 'cef.device.product', + type: 'keyword', + }, + 'cef.device.version': { + category: 'cef', + description: 'Version of the product that produced the message. ', + name: 'cef.device.version', + type: 'keyword', + }, + 'cef.device.event_class_id': { + category: 'cef', + description: 'Unique identifier of the event type. ', + name: 'cef.device.event_class_id', + type: 'keyword', + }, + 'cef.severity': { + category: 'cef', + description: + 'Importance of the event. The valid string values are Unknown, Low, Medium, High, and Very-High. The valid integer values are 0-3=Low, 4-6=Medium, 7- 8=High, and 9-10=Very-High. ', + example: 'Very-High', + name: 'cef.severity', + type: 'keyword', + }, + 'cef.name': { + category: 'cef', + description: 'Short description of the event. ', + name: 'cef.name', + type: 'keyword', + }, + 'cef.extensions.agentAddress': { + category: 'cef', + description: 'The IP address of the ArcSight connector that processed the event.', + name: 'cef.extensions.agentAddress', + type: 'ip', + }, + 'cef.extensions.agentDnsDomain': { + category: 'cef', + description: 'The DNS domain name of the ArcSight connector that processed the event.', + name: 'cef.extensions.agentDnsDomain', + type: 'keyword', + }, + 'cef.extensions.agentHostName': { + category: 'cef', + description: 'The hostname of the ArcSight connector that processed the event.', + name: 'cef.extensions.agentHostName', + type: 'keyword', + }, + 'cef.extensions.agentId': { + category: 'cef', + description: 'The agent ID of the ArcSight connector that processed the event.', + name: 'cef.extensions.agentId', + type: 'keyword', + }, + 'cef.extensions.agentMacAddress': { + category: 'cef', + description: 'The MAC address of the ArcSight connector that processed the event.', + name: 'cef.extensions.agentMacAddress', + type: 'keyword', + }, + 'cef.extensions.agentNtDomain': { + category: 'cef', + description: 'null', + name: 'cef.extensions.agentNtDomain', + type: 'keyword', + }, + 'cef.extensions.agentReceiptTime': { + category: 'cef', + description: + 'The time at which information about the event was received by the ArcSight connector.', + name: 'cef.extensions.agentReceiptTime', + type: 'date', + }, + 'cef.extensions.agentTimeZone': { + category: 'cef', + description: 'The agent time zone of the ArcSight connector that processed the event.', + name: 'cef.extensions.agentTimeZone', + type: 'keyword', + }, + 'cef.extensions.agentTranslatedAddress': { + category: 'cef', + description: 'null', + name: 'cef.extensions.agentTranslatedAddress', + type: 'ip', + }, + 'cef.extensions.agentTranslatedZoneExternalID': { + category: 'cef', + description: 'null', + name: 'cef.extensions.agentTranslatedZoneExternalID', + type: 'keyword', + }, + 'cef.extensions.agentTranslatedZoneURI': { + category: 'cef', + description: 'null', + name: 'cef.extensions.agentTranslatedZoneURI', + type: 'keyword', + }, + 'cef.extensions.agentType': { + category: 'cef', + description: 'The agent type of the ArcSight connector that processed the event', + name: 'cef.extensions.agentType', + type: 'keyword', + }, + 'cef.extensions.agentVersion': { + category: 'cef', + description: 'The version of the ArcSight connector that processed the event.', + name: 'cef.extensions.agentVersion', + type: 'keyword', + }, + 'cef.extensions.agentZoneExternalID': { + category: 'cef', + description: 'null', + name: 'cef.extensions.agentZoneExternalID', + type: 'keyword', + }, + 'cef.extensions.agentZoneURI': { + category: 'cef', + description: 'null', + name: 'cef.extensions.agentZoneURI', + type: 'keyword', + }, + 'cef.extensions.applicationProtocol': { + category: 'cef', + description: + 'Application level protocol, example values are HTTP, HTTPS, SSHv2, Telnet, POP, IMPA, IMAPS, and so on.', + name: 'cef.extensions.applicationProtocol', + type: 'keyword', + }, + 'cef.extensions.baseEventCount': { + category: 'cef', + description: + 'A count associated with this event. How many times was this same event observed? Count can be omitted if it is 1.', + name: 'cef.extensions.baseEventCount', + type: 'long', + }, + 'cef.extensions.bytesIn': { + category: 'cef', + description: + 'Number of bytes transferred inbound, relative to the source to destination relationship, meaning that data was flowing from source to destination.', + name: 'cef.extensions.bytesIn', + type: 'long', + }, + 'cef.extensions.bytesOut': { + category: 'cef', + description: + 'Number of bytes transferred outbound relative to the source to destination relationship. For example, the byte number of data flowing from the destination to the source.', + name: 'cef.extensions.bytesOut', + type: 'long', + }, + 'cef.extensions.customerExternalID': { + category: 'cef', + description: 'null', + name: 'cef.extensions.customerExternalID', + type: 'keyword', + }, + 'cef.extensions.customerURI': { + category: 'cef', + description: 'null', + name: 'cef.extensions.customerURI', + type: 'keyword', + }, + 'cef.extensions.destinationAddress': { + category: 'cef', + description: + 'Identifies the destination address that the event refers to in an IP network. The format is an IPv4 address.', + name: 'cef.extensions.destinationAddress', + type: 'ip', + }, + 'cef.extensions.destinationDnsDomain': { + category: 'cef', + description: 'The DNS domain part of the complete fully qualified domain name (FQDN).', + name: 'cef.extensions.destinationDnsDomain', + type: 'keyword', + }, + 'cef.extensions.destinationGeoLatitude': { + category: 'cef', + description: "The latitudinal value from which the destination's IP address belongs.", + name: 'cef.extensions.destinationGeoLatitude', + type: 'double', + }, + 'cef.extensions.destinationGeoLongitude': { + category: 'cef', + description: "The longitudinal value from which the destination's IP address belongs.", + name: 'cef.extensions.destinationGeoLongitude', + type: 'double', + }, + 'cef.extensions.destinationHostName': { + category: 'cef', + description: + 'Identifies the destination that an event refers to in an IP network. The format should be a fully qualified domain name (FQDN) associated with the destination node, when a node is available.', + name: 'cef.extensions.destinationHostName', + type: 'keyword', + }, + 'cef.extensions.destinationMacAddress': { + category: 'cef', + description: 'Six colon-seperated hexadecimal numbers.', + name: 'cef.extensions.destinationMacAddress', + type: 'keyword', + }, + 'cef.extensions.destinationNtDomain': { + category: 'cef', + description: 'The Windows domain name of the destination address.', + name: 'cef.extensions.destinationNtDomain', + type: 'keyword', + }, + 'cef.extensions.destinationPort': { + category: 'cef', + description: 'The valid port numbers are between 0 and 65535.', + name: 'cef.extensions.destinationPort', + type: 'long', + }, + 'cef.extensions.destinationProcessId': { + category: 'cef', + description: + 'Provides the ID of the destination process associated with the event. For example, if an event contains process ID 105, "105" is the process ID.', + name: 'cef.extensions.destinationProcessId', + type: 'long', + }, + 'cef.extensions.destinationProcessName': { + category: 'cef', + description: "The name of the event's destination process.", + name: 'cef.extensions.destinationProcessName', + type: 'keyword', + }, + 'cef.extensions.destinationServiceName': { + category: 'cef', + description: 'The service targeted by this event.', + name: 'cef.extensions.destinationServiceName', + type: 'keyword', + }, + 'cef.extensions.destinationTranslatedAddress': { + category: 'cef', + description: 'Identifies the translated destination that the event refers to in an IP network.', + name: 'cef.extensions.destinationTranslatedAddress', + type: 'ip', + }, + 'cef.extensions.destinationTranslatedPort': { + category: 'cef', + description: + 'Port after it was translated; for example, a firewall. Valid port numbers are 0 to 65535.', + name: 'cef.extensions.destinationTranslatedPort', + type: 'long', + }, + 'cef.extensions.destinationTranslatedZoneExternalID': { + category: 'cef', + description: 'null', + name: 'cef.extensions.destinationTranslatedZoneExternalID', + type: 'keyword', + }, + 'cef.extensions.destinationTranslatedZoneURI': { + category: 'cef', + description: + 'The URI for the Translated Zone that the destination asset has been assigned to in ArcSight.', + name: 'cef.extensions.destinationTranslatedZoneURI', + type: 'keyword', + }, + 'cef.extensions.destinationUserId': { + category: 'cef', + description: + 'Identifies the destination user by ID. For example, in UNIX, the root user is generally associated with user ID 0.', + name: 'cef.extensions.destinationUserId', + type: 'keyword', + }, + 'cef.extensions.destinationUserName': { + category: 'cef', + description: + "Identifies the destination user by name. This is the user associated with the event's destination. Email addresses are often mapped into the UserName fields. The recipient is a candidate to put into this field.", + name: 'cef.extensions.destinationUserName', + type: 'keyword', + }, + 'cef.extensions.destinationUserPrivileges': { + category: 'cef', + description: + 'The typical values are "Administrator", "User", and "Guest". This identifies the destination user\'s privileges. In UNIX, for example, activity executed on the root user would be identified with destinationUser Privileges of "Administrator".', + name: 'cef.extensions.destinationUserPrivileges', + type: 'keyword', + }, + 'cef.extensions.destinationZoneExternalID': { + category: 'cef', + description: 'null', + name: 'cef.extensions.destinationZoneExternalID', + type: 'keyword', + }, + 'cef.extensions.destinationZoneURI': { + category: 'cef', + description: + 'The URI for the Zone that the destination asset has been assigned to in ArcSight.', + name: 'cef.extensions.destinationZoneURI', + type: 'keyword', + }, + 'cef.extensions.deviceAction': { + category: 'cef', + description: 'Action taken by the device.', + name: 'cef.extensions.deviceAction', + type: 'keyword', + }, + 'cef.extensions.deviceAddress': { + category: 'cef', + description: 'Identifies the device address that an event refers to in an IP network.', + name: 'cef.extensions.deviceAddress', + type: 'ip', + }, + 'cef.extensions.deviceCustomFloatingPoint1Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomFloatingPoint1Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomFloatingPoint3Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomFloatingPoint3Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomFloatingPoint4Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomFloatingPoint4Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomDate1': { + category: 'cef', + description: + 'One of two timestamp fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomDate1', + type: 'date', + }, + 'cef.extensions.deviceCustomDate1Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomDate1Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomDate2': { + category: 'cef', + description: + 'One of two timestamp fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomDate2', + type: 'date', + }, + 'cef.extensions.deviceCustomDate2Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomDate2Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomFloatingPoint1': { + category: 'cef', + description: + 'One of four floating point fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomFloatingPoint1', + type: 'double', + }, + 'cef.extensions.deviceCustomFloatingPoint2': { + category: 'cef', + description: + 'One of four floating point fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomFloatingPoint2', + type: 'double', + }, + 'cef.extensions.deviceCustomFloatingPoint2Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomFloatingPoint2Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomFloatingPoint3': { + category: 'cef', + description: + 'One of four floating point fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomFloatingPoint3', + type: 'double', + }, + 'cef.extensions.deviceCustomFloatingPoint4': { + category: 'cef', + description: + 'One of four floating point fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomFloatingPoint4', + type: 'double', + }, + 'cef.extensions.deviceCustomIPv6Address1': { + category: 'cef', + description: + 'One of four IPv6 address fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomIPv6Address1', + type: 'ip', + }, + 'cef.extensions.deviceCustomIPv6Address1Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomIPv6Address1Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomIPv6Address2': { + category: 'cef', + description: + 'One of four IPv6 address fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomIPv6Address2', + type: 'ip', + }, + 'cef.extensions.deviceCustomIPv6Address2Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomIPv6Address2Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomIPv6Address3': { + category: 'cef', + description: + 'One of four IPv6 address fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomIPv6Address3', + type: 'ip', + }, + 'cef.extensions.deviceCustomIPv6Address3Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomIPv6Address3Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomIPv6Address4': { + category: 'cef', + description: + 'One of four IPv6 address fields available to map fields that do not apply to any other in this dictionary.', + name: 'cef.extensions.deviceCustomIPv6Address4', + type: 'ip', + }, + 'cef.extensions.deviceCustomIPv6Address4Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomIPv6Address4Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomNumber1': { + category: 'cef', + description: + 'One of three number fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceCustomNumber1', + type: 'long', + }, + 'cef.extensions.deviceCustomNumber1Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomNumber1Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomNumber2': { + category: 'cef', + description: + 'One of three number fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceCustomNumber2', + type: 'long', + }, + 'cef.extensions.deviceCustomNumber2Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomNumber2Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomNumber3': { + category: 'cef', + description: + 'One of three number fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceCustomNumber3', + type: 'long', + }, + 'cef.extensions.deviceCustomNumber3Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomNumber3Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString1': { + category: 'cef', + description: + 'One of six strings available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceCustomString1', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString1Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomString1Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString2': { + category: 'cef', + description: + 'One of six strings available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceCustomString2', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString2Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomString2Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString3': { + category: 'cef', + description: + 'One of six strings available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceCustomString3', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString3Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomString3Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString4': { + category: 'cef', + description: + 'One of six strings available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceCustomString4', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString4Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomString4Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString5': { + category: 'cef', + description: + 'One of six strings available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceCustomString5', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString5Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomString5Label', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString6': { + category: 'cef', + description: + 'One of six strings available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceCustomString6', + type: 'keyword', + }, + 'cef.extensions.deviceCustomString6Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceCustomString6Label', + type: 'keyword', + }, + 'cef.extensions.deviceDirection': { + category: 'cef', + description: + 'Any information about what direction the observed communication has taken. The following values are supported - "0" for inbound or "1" for outbound.', + name: 'cef.extensions.deviceDirection', + type: 'long', + }, + 'cef.extensions.deviceDnsDomain': { + category: 'cef', + description: 'The DNS domain part of the complete fully qualified domain name (FQDN).', + name: 'cef.extensions.deviceDnsDomain', + type: 'keyword', + }, + 'cef.extensions.deviceEventCategory': { + category: 'cef', + description: + 'Represents the category assigned by the originating device. Devices often use their own categorization schema to classify event. Example "/Monitor/Disk/Read".', + name: 'cef.extensions.deviceEventCategory', + type: 'keyword', + }, + 'cef.extensions.deviceExternalId': { + category: 'cef', + description: 'A name that uniquely identifies the device generating this event.', + name: 'cef.extensions.deviceExternalId', + type: 'keyword', + }, + 'cef.extensions.deviceFacility': { + category: 'cef', + description: + 'The facility generating this event. For example, Syslog has an explicit facility associated with every event.', + name: 'cef.extensions.deviceFacility', + type: 'keyword', + }, + 'cef.extensions.deviceFlexNumber1': { + category: 'cef', + description: + 'One of two alternative number fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceFlexNumber1', + type: 'long', + }, + 'cef.extensions.deviceFlexNumber1Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceFlexNumber1Label', + type: 'keyword', + }, + 'cef.extensions.deviceFlexNumber2': { + category: 'cef', + description: + 'One of two alternative number fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible.', + name: 'cef.extensions.deviceFlexNumber2', + type: 'long', + }, + 'cef.extensions.deviceFlexNumber2Label': { + category: 'cef', + description: + 'All custom fields have a corresponding label field. Each of these fields is a string and describes the purpose of the custom field.', + name: 'cef.extensions.deviceFlexNumber2Label', + type: 'keyword', + }, + 'cef.extensions.deviceHostName': { + category: 'cef', + description: + 'The format should be a fully qualified domain name (FQDN) associated with the device node, when a node is available.', + name: 'cef.extensions.deviceHostName', + type: 'keyword', + }, + 'cef.extensions.deviceInboundInterface': { + category: 'cef', + description: 'Interface on which the packet or data entered the device.', + name: 'cef.extensions.deviceInboundInterface', + type: 'keyword', + }, + 'cef.extensions.deviceMacAddress': { + category: 'cef', + description: 'Six colon-separated hexadecimal numbers.', + name: 'cef.extensions.deviceMacAddress', + type: 'keyword', + }, + 'cef.extensions.deviceNtDomain': { + category: 'cef', + description: 'The Windows domain name of the device address.', + name: 'cef.extensions.deviceNtDomain', + type: 'keyword', + }, + 'cef.extensions.deviceOutboundInterface': { + category: 'cef', + description: 'Interface on which the packet or data left the device.', + name: 'cef.extensions.deviceOutboundInterface', + type: 'keyword', + }, + 'cef.extensions.devicePayloadId': { + category: 'cef', + description: 'Unique identifier for the payload associated with the event.', + name: 'cef.extensions.devicePayloadId', + type: 'keyword', + }, + 'cef.extensions.deviceProcessId': { + category: 'cef', + description: 'Provides the ID of the process on the device generating the event.', + name: 'cef.extensions.deviceProcessId', + type: 'long', + }, + 'cef.extensions.deviceProcessName': { + category: 'cef', + description: + 'Process name associated with the event. An example might be the process generating the syslog entry in UNIX.', + name: 'cef.extensions.deviceProcessName', + type: 'keyword', + }, + 'cef.extensions.deviceReceiptTime': { + category: 'cef', + description: + 'The time at which the event related to the activity was received. The format is MMM dd yyyy HH:mm:ss or milliseconds since epoch (Jan 1st 1970)', + name: 'cef.extensions.deviceReceiptTime', + type: 'date', + }, + 'cef.extensions.deviceTimeZone': { + category: 'cef', + description: 'The time zone for the device generating the event.', + name: 'cef.extensions.deviceTimeZone', + type: 'keyword', + }, + 'cef.extensions.deviceTranslatedAddress': { + category: 'cef', + description: + 'Identifies the translated device address that the event refers to in an IP network.', + name: 'cef.extensions.deviceTranslatedAddress', + type: 'ip', + }, + 'cef.extensions.deviceTranslatedZoneExternalID': { + category: 'cef', + description: 'null', + name: 'cef.extensions.deviceTranslatedZoneExternalID', + type: 'keyword', + }, + 'cef.extensions.deviceTranslatedZoneURI': { + category: 'cef', + description: + 'The URI for the Translated Zone that the device asset has been assigned to in ArcSight.', + name: 'cef.extensions.deviceTranslatedZoneURI', + type: 'keyword', + }, + 'cef.extensions.deviceZoneExternalID': { + category: 'cef', + description: 'null', + name: 'cef.extensions.deviceZoneExternalID', + type: 'keyword', + }, + 'cef.extensions.deviceZoneURI': { + category: 'cef', + description: 'Thee URI for the Zone that the device asset has been assigned to in ArcSight.', + name: 'cef.extensions.deviceZoneURI', + type: 'keyword', + }, + 'cef.extensions.endTime': { + category: 'cef', + description: + 'The time at which the activity related to the event ended. The format is MMM dd yyyy HH:mm:ss or milliseconds since epoch (Jan 1st1970). An example would be reporting the end of a session.', + name: 'cef.extensions.endTime', + type: 'date', + }, + 'cef.extensions.eventId': { + category: 'cef', + description: 'This is a unique ID that ArcSight assigns to each event.', + name: 'cef.extensions.eventId', + type: 'long', + }, + 'cef.extensions.eventOutcome': { + category: 'cef', + description: "Displays the outcome, usually as 'success' or 'failure'.", + name: 'cef.extensions.eventOutcome', + type: 'keyword', + }, + 'cef.extensions.externalId': { + category: 'cef', + description: + 'The ID used by an originating device. They are usually increasing numbers, associated with events.', + name: 'cef.extensions.externalId', + type: 'keyword', + }, + 'cef.extensions.fileCreateTime': { + category: 'cef', + description: 'Time when the file was created.', + name: 'cef.extensions.fileCreateTime', + type: 'date', + }, + 'cef.extensions.fileHash': { + category: 'cef', + description: 'Hash of a file.', + name: 'cef.extensions.fileHash', + type: 'keyword', + }, + 'cef.extensions.fileId': { + category: 'cef', + description: 'An ID associated with a file could be the inode.', + name: 'cef.extensions.fileId', + type: 'keyword', + }, + 'cef.extensions.fileModificationTime': { + category: 'cef', + description: 'Time when the file was last modified.', + name: 'cef.extensions.fileModificationTime', + type: 'date', + }, + 'cef.extensions.filename': { + category: 'cef', + description: 'Name of the file only (without its path).', + name: 'cef.extensions.filename', + type: 'keyword', + }, + 'cef.extensions.filePath': { + category: 'cef', + description: 'Full path to the file, including file name itself.', + name: 'cef.extensions.filePath', + type: 'keyword', + }, + 'cef.extensions.filePermission': { + category: 'cef', + description: 'Permissions of the file.', + name: 'cef.extensions.filePermission', + type: 'keyword', + }, + 'cef.extensions.fileSize': { + category: 'cef', + description: 'Size of the file.', + name: 'cef.extensions.fileSize', + type: 'long', + }, + 'cef.extensions.fileType': { + category: 'cef', + description: 'Type of file (pipe, socket, etc.)', + name: 'cef.extensions.fileType', + type: 'keyword', + }, + 'cef.extensions.flexDate1': { + category: 'cef', + description: + 'A timestamp field available to map a timestamp that does not apply to any other defined timestamp field in this dictionary. Use all flex fields sparingly and seek a more specific, dictionary supplied field when possible. These fields are typically reserved for customer use and should not be set by vendors unless necessary.', + name: 'cef.extensions.flexDate1', + type: 'date', + }, + 'cef.extensions.flexDate1Label': { + category: 'cef', + description: 'The label field is a string and describes the purpose of the flex field.', + name: 'cef.extensions.flexDate1Label', + type: 'keyword', + }, + 'cef.extensions.flexString1': { + category: 'cef', + description: + 'One of four floating point fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible. These fields are typically reserved for customer use and should not be set by vendors unless necessary.', + name: 'cef.extensions.flexString1', + type: 'keyword', + }, + 'cef.extensions.flexString2': { + category: 'cef', + description: + 'One of four floating point fields available to map fields that do not apply to any other in this dictionary. Use sparingly and seek a more specific, dictionary supplied field when possible. These fields are typically reserved for customer use and should not be set by vendors unless necessary.', + name: 'cef.extensions.flexString2', + type: 'keyword', + }, + 'cef.extensions.flexString1Label': { + category: 'cef', + description: 'The label field is a string and describes the purpose of the flex field.', + name: 'cef.extensions.flexString1Label', + type: 'keyword', + }, + 'cef.extensions.flexString2Label': { + category: 'cef', + description: 'The label field is a string and describes the purpose of the flex field.', + name: 'cef.extensions.flexString2Label', + type: 'keyword', + }, + 'cef.extensions.message': { + category: 'cef', + description: + 'An arbitrary message giving more details about the event. Multi-line entries can be produced by using \\n as the new line separator.', + name: 'cef.extensions.message', + type: 'keyword', + }, + 'cef.extensions.oldFileCreateTime': { + category: 'cef', + description: 'Time when old file was created.', + name: 'cef.extensions.oldFileCreateTime', + type: 'date', + }, + 'cef.extensions.oldFileHash': { + category: 'cef', + description: 'Hash of the old file.', + name: 'cef.extensions.oldFileHash', + type: 'keyword', + }, + 'cef.extensions.oldFileId': { + category: 'cef', + description: 'An ID associated with the old file could be the inode.', + name: 'cef.extensions.oldFileId', + type: 'keyword', + }, + 'cef.extensions.oldFileModificationTime': { + category: 'cef', + description: 'Time when old file was last modified.', + name: 'cef.extensions.oldFileModificationTime', + type: 'date', + }, + 'cef.extensions.oldFileName': { + category: 'cef', + description: 'Name of the old file.', + name: 'cef.extensions.oldFileName', + type: 'keyword', + }, + 'cef.extensions.oldFilePath': { + category: 'cef', + description: 'Full path to the old file, including the file name itself.', + name: 'cef.extensions.oldFilePath', + type: 'keyword', + }, + 'cef.extensions.oldFilePermission': { + category: 'cef', + description: 'Permissions of the old file.', + name: 'cef.extensions.oldFilePermission', + type: 'keyword', + }, + 'cef.extensions.oldFileSize': { + category: 'cef', + description: 'Size of the old file.', + name: 'cef.extensions.oldFileSize', + type: 'long', + }, + 'cef.extensions.oldFileType': { + category: 'cef', + description: 'Type of the old file (pipe, socket, etc.)', + name: 'cef.extensions.oldFileType', + type: 'keyword', + }, + 'cef.extensions.rawEvent': { + category: 'cef', + description: 'null', + name: 'cef.extensions.rawEvent', + type: 'keyword', + }, + 'cef.extensions.Reason': { + category: 'cef', + description: + 'The reason an audit event was generated. For example "bad password" or "unknown user". This could also be an error or return code. Example "0x1234".', + name: 'cef.extensions.Reason', + type: 'keyword', + }, + 'cef.extensions.requestClientApplication': { + category: 'cef', + description: 'The User-Agent associated with the request.', + name: 'cef.extensions.requestClientApplication', + type: 'keyword', + }, + 'cef.extensions.requestContext': { + category: 'cef', + description: + 'Description of the content from which the request originated (for example, HTTP Referrer)', + name: 'cef.extensions.requestContext', + type: 'keyword', + }, + 'cef.extensions.requestCookies': { + category: 'cef', + description: 'Cookies associated with the request.', + name: 'cef.extensions.requestCookies', + type: 'keyword', + }, + 'cef.extensions.requestMethod': { + category: 'cef', + description: 'The HTTP method used to access a URL.', + name: 'cef.extensions.requestMethod', + type: 'keyword', + }, + 'cef.extensions.requestUrl': { + category: 'cef', + description: + 'In the case of an HTTP request, this field contains the URL accessed. The URL should contain the protocol as well.', + name: 'cef.extensions.requestUrl', + type: 'keyword', + }, + 'cef.extensions.sourceAddress': { + category: 'cef', + description: 'Identifies the source that an event refers to in an IP network.', + name: 'cef.extensions.sourceAddress', + type: 'ip', + }, + 'cef.extensions.sourceDnsDomain': { + category: 'cef', + description: 'The DNS domain part of the complete fully qualified domain name (FQDN).', + name: 'cef.extensions.sourceDnsDomain', + type: 'keyword', + }, + 'cef.extensions.sourceGeoLatitude': { + category: 'cef', + description: 'null', + name: 'cef.extensions.sourceGeoLatitude', + type: 'double', + }, + 'cef.extensions.sourceGeoLongitude': { + category: 'cef', + description: 'null', + name: 'cef.extensions.sourceGeoLongitude', + type: 'double', + }, + 'cef.extensions.sourceHostName': { + category: 'cef', + description: + "Identifies the source that an event refers to in an IP network. The format should be a fully qualified domain name (FQDN) associated with the source node, when a mode is available. Examples: 'host' or 'host.domain.com'. ", + name: 'cef.extensions.sourceHostName', + type: 'keyword', + }, + 'cef.extensions.sourceMacAddress': { + category: 'cef', + description: 'Six colon-separated hexadecimal numbers.', + example: '00:0d:60:af:1b:61', + name: 'cef.extensions.sourceMacAddress', + type: 'keyword', + }, + 'cef.extensions.sourceNtDomain': { + category: 'cef', + description: 'The Windows domain name for the source address.', + name: 'cef.extensions.sourceNtDomain', + type: 'keyword', + }, + 'cef.extensions.sourcePort': { + category: 'cef', + description: 'The valid port numbers are 0 to 65535.', + name: 'cef.extensions.sourcePort', + type: 'long', + }, + 'cef.extensions.sourceProcessId': { + category: 'cef', + description: 'The ID of the source process associated with the event.', + name: 'cef.extensions.sourceProcessId', + type: 'long', + }, + 'cef.extensions.sourceProcessName': { + category: 'cef', + description: "The name of the event's source process.", + name: 'cef.extensions.sourceProcessName', + type: 'keyword', + }, + 'cef.extensions.sourceServiceName': { + category: 'cef', + description: 'The service that is responsible for generating this event.', + name: 'cef.extensions.sourceServiceName', + type: 'keyword', + }, + 'cef.extensions.sourceTranslatedAddress': { + category: 'cef', + description: 'Identifies the translated source that the event refers to in an IP network.', + name: 'cef.extensions.sourceTranslatedAddress', + type: 'ip', + }, + 'cef.extensions.sourceTranslatedPort': { + category: 'cef', + description: + 'A port number after being translated by, for example, a firewall. Valid port numbers are 0 to 65535.', + name: 'cef.extensions.sourceTranslatedPort', + type: 'long', + }, + 'cef.extensions.sourceTranslatedZoneExternalID': { + category: 'cef', + description: 'null', + name: 'cef.extensions.sourceTranslatedZoneExternalID', + type: 'keyword', + }, + 'cef.extensions.sourceTranslatedZoneURI': { + category: 'cef', + description: + 'The URI for the Translated Zone that the destination asset has been assigned to in ArcSight.', + name: 'cef.extensions.sourceTranslatedZoneURI', + type: 'keyword', + }, + 'cef.extensions.sourceUserId': { + category: 'cef', + description: + 'Identifies the source user by ID. This is the user associated with the source of the event. For example, in UNIX, the root user is generally associated with user ID 0.', + name: 'cef.extensions.sourceUserId', + type: 'keyword', + }, + 'cef.extensions.sourceUserName': { + category: 'cef', + description: + 'Identifies the source user by name. Email addresses are also mapped into the UserName fields. The sender is a candidate to put into this field.', + name: 'cef.extensions.sourceUserName', + type: 'keyword', + }, + 'cef.extensions.sourceUserPrivileges': { + category: 'cef', + description: + 'The typical values are "Administrator", "User", and "Guest". It identifies the source user\'s privileges. In UNIX, for example, activity executed by the root user would be identified with "Administrator".', + name: 'cef.extensions.sourceUserPrivileges', + type: 'keyword', + }, + 'cef.extensions.sourceZoneExternalID': { + category: 'cef', + description: 'null', + name: 'cef.extensions.sourceZoneExternalID', + type: 'keyword', + }, + 'cef.extensions.sourceZoneURI': { + category: 'cef', + description: 'The URI for the Zone that the source asset has been assigned to in ArcSight.', + name: 'cef.extensions.sourceZoneURI', + type: 'keyword', + }, + 'cef.extensions.startTime': { + category: 'cef', + description: + 'The time when the activity the event referred to started. The format is MMM dd yyyy HH:mm:ss or milliseconds since epoch (Jan 1st 1970)', + name: 'cef.extensions.startTime', + type: 'date', + }, + 'cef.extensions.transportProtocol': { + category: 'cef', + description: + 'Identifies the Layer-4 protocol used. The possible values are protocols such as TCP or UDP.', + name: 'cef.extensions.transportProtocol', + type: 'keyword', + }, + 'cef.extensions.type': { + category: 'cef', + description: + '0 means base event, 1 means aggregated, 2 means correlation, and 3 means action. This field can be omitted for base events (type 0).', + name: 'cef.extensions.type', + type: 'long', + }, + 'cef.extensions.categoryDeviceType': { + category: 'cef', + description: 'Device type. Examples - Proxy, IDS, Web Server', + name: 'cef.extensions.categoryDeviceType', + type: 'keyword', + }, + 'cef.extensions.categoryObject': { + category: 'cef', + description: + 'Object that the event is about. For example it can be an operating sytem, database, file, etc.', + name: 'cef.extensions.categoryObject', + type: 'keyword', + }, + 'cef.extensions.categoryBehavior': { + category: 'cef', + description: + "Action or a behavior associated with an event. It's what is being done to the object.", + name: 'cef.extensions.categoryBehavior', + type: 'keyword', + }, + 'cef.extensions.categoryTechnique': { + category: 'cef', + description: 'Technique being used (e.g. /DoS).', + name: 'cef.extensions.categoryTechnique', + type: 'keyword', + }, + 'cef.extensions.categoryDeviceGroup': { + category: 'cef', + description: 'General device group like Firewall.', + name: 'cef.extensions.categoryDeviceGroup', + type: 'keyword', + }, + 'cef.extensions.categorySignificance': { + category: 'cef', + description: 'Characterization of the importance of the event.', + name: 'cef.extensions.categorySignificance', + type: 'keyword', + }, + 'cef.extensions.categoryOutcome': { + category: 'cef', + description: 'Outcome of the event (e.g. sucess, failure, or attempt).', + name: 'cef.extensions.categoryOutcome', + type: 'keyword', + }, + 'cef.extensions.managerReceiptTime': { + category: 'cef', + description: 'When the Arcsight ESM received the event.', + name: 'cef.extensions.managerReceiptTime', + type: 'date', + }, + 'source.service.name': { + category: 'source', + description: 'Service that is the source of the event.', + name: 'source.service.name', + type: 'keyword', + }, + 'destination.service.name': { + category: 'destination', + description: 'Service that is the target of the event.', + name: 'destination.service.name', + type: 'keyword', + }, + type: { + category: 'base', + description: + 'The type of the transaction (for example, HTTP, MySQL, Redis, or RUM) or "flow" in case of flows. ', + name: 'type', + }, + 'server.process.name': { + category: 'server', + description: 'The name of the process that served the transaction. ', + name: 'server.process.name', + }, + 'server.process.args': { + category: 'server', + description: 'The command-line of the process that served the transaction. ', + name: 'server.process.args', + }, + 'server.process.executable': { + category: 'server', + description: 'Absolute path to the server process executable. ', + name: 'server.process.executable', + }, + 'server.process.working_directory': { + category: 'server', + description: 'The working directory of the server process. ', + name: 'server.process.working_directory', + }, + 'server.process.start': { + category: 'server', + description: 'The time the server process started. ', + name: 'server.process.start', + }, + 'client.process.name': { + category: 'client', + description: 'The name of the process that initiated the transaction. ', + name: 'client.process.name', + }, + 'client.process.args': { + category: 'client', + description: 'The command-line of the process that initiated the transaction. ', + name: 'client.process.args', + }, + 'client.process.executable': { + category: 'client', + description: 'Absolute path to the client process executable. ', + name: 'client.process.executable', + }, + 'client.process.working_directory': { + category: 'client', + description: 'The working directory of the client process. ', + name: 'client.process.working_directory', + }, + 'client.process.start': { + category: 'client', + description: 'The time the client process started. ', + name: 'client.process.start', + }, + real_ip: { + category: 'base', + description: + 'If the server initiating the transaction is a proxy, this field contains the original client IP address. For HTTP, for example, the IP address extracted from a configurable HTTP header, by default `X-Forwarded-For`. Unless this field is disabled, it always has a value, and it matches the `client_ip` for non proxy clients. ', + name: 'real_ip', + type: 'alias', + }, + transport: { + category: 'base', + description: + 'The transport protocol used for the transaction. If not specified, then tcp is assumed. ', + name: 'transport', + type: 'alias', + }, + 'flow.final': { + category: 'flow', + description: + 'Indicates if event is last event in flow. If final is false, the event reports an intermediate flow state only. ', + name: 'flow.final', + type: 'boolean', + }, + 'flow.id': { + category: 'flow', + description: 'Internal flow ID based on connection meta data and address. ', + name: 'flow.id', + }, + 'flow.vlan': { + category: 'flow', + description: + "VLAN identifier from the 802.1q frame. In case of a multi-tagged frame this field will be an array with the outer tag's VLAN identifier listed first. ", + name: 'flow.vlan', + type: 'long', + }, + flow_id: { + category: 'base', + name: 'flow_id', + type: 'alias', + }, + final: { + category: 'base', + name: 'final', + type: 'alias', + }, + vlan: { + category: 'base', + name: 'vlan', + type: 'alias', + }, + 'source.stats.net_bytes_total': { + category: 'source', + name: 'source.stats.net_bytes_total', + type: 'alias', + }, + 'source.stats.net_packets_total': { + category: 'source', + name: 'source.stats.net_packets_total', + type: 'alias', + }, + 'dest.stats.net_bytes_total': { + category: 'dest', + name: 'dest.stats.net_bytes_total', + type: 'alias', + }, + 'dest.stats.net_packets_total': { + category: 'dest', + name: 'dest.stats.net_packets_total', + type: 'alias', + }, + status: { + category: 'base', + description: + 'The high level status of the transaction. The way to compute this value depends on the protocol, but the result has a meaning independent of the protocol. ', + name: 'status', + }, + method: { + category: 'base', + description: + 'The command/verb/method of the transaction. For HTTP, this is the method name (GET, POST, PUT, and so on), for SQL this is the verb (SELECT, UPDATE, DELETE, and so on). ', + name: 'method', + }, + resource: { + category: 'base', + description: + 'The logical resource that this transaction refers to. For HTTP, this is the URL path up to the last slash (/). For example, if the URL is `/users/1`, the resource is `/users`. For databases, the resource is typically the table name. The field is not filled for all transaction types. ', + name: 'resource', + }, + path: { + category: 'base', + description: + 'The path the transaction refers to. For HTTP, this is the URL. For SQL databases, this is the table name. For key-value stores, this is the key. ', + name: 'path', + }, + query: { + category: 'base', + description: + 'The query in a human readable format. For HTTP, it will typically be something like `GET /users/_search?name=test`. For MySQL, it is something like `SELECT id from users where name=test`. ', + name: 'query', + type: 'keyword', + }, + params: { + category: 'base', + description: + 'The request parameters. For HTTP, these are the POST or GET parameters. For Thrift-RPC, these are the parameters from the request. ', + name: 'params', + type: 'text', + }, + notes: { + category: 'base', + description: + 'Messages from Packetbeat itself. This field usually contains error messages for interpreting the raw data. This information can be helpful for troubleshooting. ', + name: 'notes', + type: 'alias', + }, + request: { + category: 'base', + description: + 'For text protocols, this is the request as seen on the wire (application layer only). For binary protocols this is our representation of the request. ', + name: 'request', + type: 'text', + }, + response: { + category: 'base', + description: + 'For text protocols, this is the response as seen on the wire (application layer only). For binary protocols this is our representation of the request. ', + name: 'response', + type: 'text', + }, + bytes_in: { + category: 'base', + description: + 'The number of bytes of the request. Note that this size is the application layer message length, without the length of the IP or TCP headers. ', + name: 'bytes_in', + type: 'alias', + }, + bytes_out: { + category: 'base', + description: + 'The number of bytes of the response. Note that this size is the application layer message length, without the length of the IP or TCP headers. ', + name: 'bytes_out', + type: 'alias', + }, + 'amqp.reply-code': { + category: 'amqp', + description: 'AMQP reply code to an error, similar to http reply-code ', + example: 404, + name: 'amqp.reply-code', + type: 'long', + }, + 'amqp.reply-text': { + category: 'amqp', + description: 'Text explaining the error. ', + name: 'amqp.reply-text', + type: 'keyword', + }, + 'amqp.class-id': { + category: 'amqp', + description: 'Failing method class. ', + name: 'amqp.class-id', + type: 'long', + }, + 'amqp.method-id': { + category: 'amqp', + description: 'Failing method ID. ', + name: 'amqp.method-id', + type: 'long', + }, + 'amqp.exchange': { + category: 'amqp', + description: 'Name of the exchange. ', + name: 'amqp.exchange', + type: 'keyword', + }, + 'amqp.exchange-type': { + category: 'amqp', + description: 'Exchange type. ', + example: 'fanout', + name: 'amqp.exchange-type', + type: 'keyword', + }, + 'amqp.passive': { + category: 'amqp', + description: 'If set, do not create exchange/queue. ', + name: 'amqp.passive', + type: 'boolean', + }, + 'amqp.durable': { + category: 'amqp', + description: 'If set, request a durable exchange/queue. ', + name: 'amqp.durable', + type: 'boolean', + }, + 'amqp.exclusive': { + category: 'amqp', + description: 'If set, request an exclusive queue. ', + name: 'amqp.exclusive', + type: 'boolean', + }, + 'amqp.auto-delete': { + category: 'amqp', + description: 'If set, auto-delete queue when unused. ', + name: 'amqp.auto-delete', + type: 'boolean', + }, + 'amqp.no-wait': { + category: 'amqp', + description: 'If set, the server will not respond to the method. ', + name: 'amqp.no-wait', + type: 'boolean', + }, + 'amqp.consumer-tag': { + category: 'amqp', + description: 'Identifier for the consumer, valid within the current channel. ', + name: 'amqp.consumer-tag', + }, + 'amqp.delivery-tag': { + category: 'amqp', + description: 'The server-assigned and channel-specific delivery tag. ', + name: 'amqp.delivery-tag', + type: 'long', + }, + 'amqp.message-count': { + category: 'amqp', + description: + 'The number of messages in the queue, which will be zero for newly-declared queues. ', + name: 'amqp.message-count', + type: 'long', + }, + 'amqp.consumer-count': { + category: 'amqp', + description: 'The number of consumers of a queue. ', + name: 'amqp.consumer-count', + type: 'long', + }, + 'amqp.routing-key': { + category: 'amqp', + description: 'Message routing key. ', + name: 'amqp.routing-key', + type: 'keyword', + }, + 'amqp.no-ack': { + category: 'amqp', + description: 'If set, the server does not expect acknowledgements for messages. ', + name: 'amqp.no-ack', + type: 'boolean', + }, + 'amqp.no-local': { + category: 'amqp', + description: + 'If set, the server will not send messages to the connection that published them. ', + name: 'amqp.no-local', + type: 'boolean', + }, + 'amqp.if-unused': { + category: 'amqp', + description: 'Delete only if unused. ', + name: 'amqp.if-unused', + type: 'boolean', + }, + 'amqp.if-empty': { + category: 'amqp', + description: 'Delete only if empty. ', + name: 'amqp.if-empty', + type: 'boolean', + }, + 'amqp.queue': { + category: 'amqp', + description: 'The queue name identifies the queue within the vhost. ', + name: 'amqp.queue', + type: 'keyword', + }, + 'amqp.redelivered': { + category: 'amqp', + description: + 'Indicates that the message has been previously delivered to this or another client. ', + name: 'amqp.redelivered', + type: 'boolean', + }, + 'amqp.multiple': { + category: 'amqp', + description: 'Acknowledge multiple messages. ', + name: 'amqp.multiple', + type: 'boolean', + }, + 'amqp.arguments': { + category: 'amqp', + description: 'Optional additional arguments passed to some methods. Can be of various types. ', + name: 'amqp.arguments', + type: 'object', + }, + 'amqp.mandatory': { + category: 'amqp', + description: 'Indicates mandatory routing. ', + name: 'amqp.mandatory', + type: 'boolean', + }, + 'amqp.immediate': { + category: 'amqp', + description: 'Request immediate delivery. ', + name: 'amqp.immediate', + type: 'boolean', + }, + 'amqp.content-type': { + category: 'amqp', + description: 'MIME content type. ', + example: 'text/plain', + name: 'amqp.content-type', + type: 'keyword', + }, + 'amqp.content-encoding': { + category: 'amqp', + description: 'MIME content encoding. ', + name: 'amqp.content-encoding', + type: 'keyword', + }, + 'amqp.headers': { + category: 'amqp', + description: 'Message header field table. ', + name: 'amqp.headers', + type: 'object', + }, + 'amqp.delivery-mode': { + category: 'amqp', + description: 'Non-persistent (1) or persistent (2). ', + name: 'amqp.delivery-mode', + type: 'keyword', + }, + 'amqp.priority': { + category: 'amqp', + description: 'Message priority, 0 to 9. ', + name: 'amqp.priority', + type: 'long', + }, + 'amqp.correlation-id': { + category: 'amqp', + description: 'Application correlation identifier. ', + name: 'amqp.correlation-id', + type: 'keyword', + }, + 'amqp.reply-to': { + category: 'amqp', + description: 'Address to reply to. ', + name: 'amqp.reply-to', + type: 'keyword', + }, + 'amqp.expiration': { + category: 'amqp', + description: 'Message expiration specification. ', + name: 'amqp.expiration', + type: 'keyword', + }, + 'amqp.message-id': { + category: 'amqp', + description: 'Application message identifier. ', + name: 'amqp.message-id', + type: 'keyword', + }, + 'amqp.timestamp': { + category: 'amqp', + description: 'Message timestamp. ', + name: 'amqp.timestamp', + type: 'keyword', + }, + 'amqp.type': { + category: 'amqp', + description: 'Message type name. ', + name: 'amqp.type', + type: 'keyword', + }, + 'amqp.user-id': { + category: 'amqp', + description: 'Creating user id. ', + name: 'amqp.user-id', + type: 'keyword', + }, + 'amqp.app-id': { + category: 'amqp', + description: 'Creating application id. ', + name: 'amqp.app-id', + type: 'keyword', + }, + no_request: { + category: 'base', + name: 'no_request', + type: 'alias', + }, + 'cassandra.no_request': { + category: 'cassandra', + description: 'Indicates that there is no request because this is a PUSH message. ', + name: 'cassandra.no_request', + type: 'boolean', + }, + 'cassandra.request.headers.version': { + category: 'cassandra', + description: 'The version of the protocol.', + name: 'cassandra.request.headers.version', + type: 'long', + }, + 'cassandra.request.headers.flags': { + category: 'cassandra', + description: 'Flags applying to this frame.', + name: 'cassandra.request.headers.flags', + type: 'keyword', + }, + 'cassandra.request.headers.stream': { + category: 'cassandra', + description: + 'A frame has a stream id. If a client sends a request message with the stream id X, it is guaranteed that the stream id of the response to that message will be X.', + name: 'cassandra.request.headers.stream', + type: 'keyword', + }, + 'cassandra.request.headers.op': { + category: 'cassandra', + description: 'An operation type that distinguishes the actual message.', + name: 'cassandra.request.headers.op', + type: 'keyword', + }, + 'cassandra.request.headers.length': { + category: 'cassandra', + description: + 'A integer representing the length of the body of the frame (a frame is limited to 256MB in length).', + name: 'cassandra.request.headers.length', + type: 'long', + }, + 'cassandra.request.query': { + category: 'cassandra', + description: 'The CQL query which client send to cassandra.', + name: 'cassandra.request.query', + type: 'keyword', + }, + 'cassandra.response.headers.version': { + category: 'cassandra', + description: 'The version of the protocol.', + name: 'cassandra.response.headers.version', + type: 'long', + }, + 'cassandra.response.headers.flags': { + category: 'cassandra', + description: 'Flags applying to this frame.', + name: 'cassandra.response.headers.flags', + type: 'keyword', + }, + 'cassandra.response.headers.stream': { + category: 'cassandra', + description: + 'A frame has a stream id. If a client sends a request message with the stream id X, it is guaranteed that the stream id of the response to that message will be X.', + name: 'cassandra.response.headers.stream', + type: 'keyword', + }, + 'cassandra.response.headers.op': { + category: 'cassandra', + description: 'An operation type that distinguishes the actual message.', + name: 'cassandra.response.headers.op', + type: 'keyword', + }, + 'cassandra.response.headers.length': { + category: 'cassandra', + description: + 'A integer representing the length of the body of the frame (a frame is limited to 256MB in length).', + name: 'cassandra.response.headers.length', + type: 'long', + }, + 'cassandra.response.result.type': { + category: 'cassandra', + description: 'Cassandra result type.', + name: 'cassandra.response.result.type', + type: 'keyword', + }, + 'cassandra.response.result.rows.num_rows': { + category: 'cassandra', + description: 'Representing the number of rows present in this result.', + name: 'cassandra.response.result.rows.num_rows', + type: 'long', + }, + 'cassandra.response.result.rows.meta.keyspace': { + category: 'cassandra', + description: 'Only present after set Global_tables_spec, the keyspace name.', + name: 'cassandra.response.result.rows.meta.keyspace', + type: 'keyword', + }, + 'cassandra.response.result.rows.meta.table': { + category: 'cassandra', + description: 'Only present after set Global_tables_spec, the table name.', + name: 'cassandra.response.result.rows.meta.table', + type: 'keyword', + }, + 'cassandra.response.result.rows.meta.flags': { + category: 'cassandra', + description: 'Provides information on the formatting of the remaining information.', + name: 'cassandra.response.result.rows.meta.flags', + type: 'keyword', + }, + 'cassandra.response.result.rows.meta.col_count': { + category: 'cassandra', + description: + 'Representing the number of columns selected by the query that produced this result.', + name: 'cassandra.response.result.rows.meta.col_count', + type: 'long', + }, + 'cassandra.response.result.rows.meta.pkey_columns': { + category: 'cassandra', + description: 'Representing the PK columns index and counts.', + name: 'cassandra.response.result.rows.meta.pkey_columns', + type: 'long', + }, + 'cassandra.response.result.rows.meta.paging_state': { + category: 'cassandra', + description: + 'The paging_state is a bytes value that should be used in QUERY/EXECUTE to continue paging and retrieve the remainder of the result for this query.', + name: 'cassandra.response.result.rows.meta.paging_state', + type: 'keyword', + }, + 'cassandra.response.result.keyspace': { + category: 'cassandra', + description: 'Indicating the name of the keyspace that has been set.', + name: 'cassandra.response.result.keyspace', + type: 'keyword', + }, + 'cassandra.response.result.schema_change.change': { + category: 'cassandra', + description: 'Representing the type of changed involved.', + name: 'cassandra.response.result.schema_change.change', + type: 'keyword', + }, + 'cassandra.response.result.schema_change.keyspace': { + category: 'cassandra', + description: 'This describes which keyspace has changed.', + name: 'cassandra.response.result.schema_change.keyspace', + type: 'keyword', + }, + 'cassandra.response.result.schema_change.table': { + category: 'cassandra', + description: 'This describes which table has changed.', + name: 'cassandra.response.result.schema_change.table', + type: 'keyword', + }, + 'cassandra.response.result.schema_change.object': { + category: 'cassandra', + description: + 'This describes the name of said affected object (either the table, user type, function, or aggregate name).', + name: 'cassandra.response.result.schema_change.object', + type: 'keyword', + }, + 'cassandra.response.result.schema_change.target': { + category: 'cassandra', + description: 'Target could be "FUNCTION" or "AGGREGATE", multiple arguments.', + name: 'cassandra.response.result.schema_change.target', + type: 'keyword', + }, + 'cassandra.response.result.schema_change.name': { + category: 'cassandra', + description: 'The function/aggregate name.', + name: 'cassandra.response.result.schema_change.name', + type: 'keyword', + }, + 'cassandra.response.result.schema_change.args': { + category: 'cassandra', + description: 'One string for each argument type (as CQL type).', + name: 'cassandra.response.result.schema_change.args', + type: 'keyword', + }, + 'cassandra.response.result.prepared.prepared_id': { + category: 'cassandra', + description: 'Representing the prepared query ID.', + name: 'cassandra.response.result.prepared.prepared_id', + type: 'keyword', + }, + 'cassandra.response.result.prepared.req_meta.keyspace': { + category: 'cassandra', + description: 'Only present after set Global_tables_spec, the keyspace name.', + name: 'cassandra.response.result.prepared.req_meta.keyspace', + type: 'keyword', + }, + 'cassandra.response.result.prepared.req_meta.table': { + category: 'cassandra', + description: 'Only present after set Global_tables_spec, the table name.', + name: 'cassandra.response.result.prepared.req_meta.table', + type: 'keyword', + }, + 'cassandra.response.result.prepared.req_meta.flags': { + category: 'cassandra', + description: 'Provides information on the formatting of the remaining information.', + name: 'cassandra.response.result.prepared.req_meta.flags', + type: 'keyword', + }, + 'cassandra.response.result.prepared.req_meta.col_count': { + category: 'cassandra', + description: + 'Representing the number of columns selected by the query that produced this result.', + name: 'cassandra.response.result.prepared.req_meta.col_count', + type: 'long', + }, + 'cassandra.response.result.prepared.req_meta.pkey_columns': { + category: 'cassandra', + description: 'Representing the PK columns index and counts.', + name: 'cassandra.response.result.prepared.req_meta.pkey_columns', + type: 'long', + }, + 'cassandra.response.result.prepared.req_meta.paging_state': { + category: 'cassandra', + description: + 'The paging_state is a bytes value that should be used in QUERY/EXECUTE to continue paging and retrieve the remainder of the result for this query.', + name: 'cassandra.response.result.prepared.req_meta.paging_state', + type: 'keyword', + }, + 'cassandra.response.result.prepared.resp_meta.keyspace': { + category: 'cassandra', + description: 'Only present after set Global_tables_spec, the keyspace name.', + name: 'cassandra.response.result.prepared.resp_meta.keyspace', + type: 'keyword', + }, + 'cassandra.response.result.prepared.resp_meta.table': { + category: 'cassandra', + description: 'Only present after set Global_tables_spec, the table name.', + name: 'cassandra.response.result.prepared.resp_meta.table', + type: 'keyword', + }, + 'cassandra.response.result.prepared.resp_meta.flags': { + category: 'cassandra', + description: 'Provides information on the formatting of the remaining information.', + name: 'cassandra.response.result.prepared.resp_meta.flags', + type: 'keyword', + }, + 'cassandra.response.result.prepared.resp_meta.col_count': { + category: 'cassandra', + description: + 'Representing the number of columns selected by the query that produced this result.', + name: 'cassandra.response.result.prepared.resp_meta.col_count', + type: 'long', + }, + 'cassandra.response.result.prepared.resp_meta.pkey_columns': { + category: 'cassandra', + description: 'Representing the PK columns index and counts.', + name: 'cassandra.response.result.prepared.resp_meta.pkey_columns', + type: 'long', + }, + 'cassandra.response.result.prepared.resp_meta.paging_state': { + category: 'cassandra', + description: + 'The paging_state is a bytes value that should be used in QUERY/EXECUTE to continue paging and retrieve the remainder of the result for this query.', + name: 'cassandra.response.result.prepared.resp_meta.paging_state', + type: 'keyword', + }, + 'cassandra.response.supported': { + category: 'cassandra', + description: + 'Indicates which startup options are supported by the server. This message comes as a response to an OPTIONS message.', + name: 'cassandra.response.supported', + type: 'object', + }, + 'cassandra.response.authentication.class': { + category: 'cassandra', + description: 'Indicates the full class name of the IAuthenticator in use', + name: 'cassandra.response.authentication.class', + type: 'keyword', + }, + 'cassandra.response.warnings': { + category: 'cassandra', + description: 'The text of the warnings, only occur when Warning flag was set.', + name: 'cassandra.response.warnings', + type: 'keyword', + }, + 'cassandra.response.event.type': { + category: 'cassandra', + description: 'Representing the event type.', + name: 'cassandra.response.event.type', + type: 'keyword', + }, + 'cassandra.response.event.change': { + category: 'cassandra', + description: + 'The message corresponding respectively to the type of change followed by the address of the new/removed node.', + name: 'cassandra.response.event.change', + type: 'keyword', + }, + 'cassandra.response.event.host': { + category: 'cassandra', + description: 'Representing the node ip.', + name: 'cassandra.response.event.host', + type: 'keyword', + }, + 'cassandra.response.event.port': { + category: 'cassandra', + description: 'Representing the node port.', + name: 'cassandra.response.event.port', + type: 'long', + }, + 'cassandra.response.event.schema_change.change': { + category: 'cassandra', + description: 'Representing the type of changed involved.', + name: 'cassandra.response.event.schema_change.change', + type: 'keyword', + }, + 'cassandra.response.event.schema_change.keyspace': { + category: 'cassandra', + description: 'This describes which keyspace has changed.', + name: 'cassandra.response.event.schema_change.keyspace', + type: 'keyword', + }, + 'cassandra.response.event.schema_change.table': { + category: 'cassandra', + description: 'This describes which table has changed.', + name: 'cassandra.response.event.schema_change.table', + type: 'keyword', + }, + 'cassandra.response.event.schema_change.object': { + category: 'cassandra', + description: + 'This describes the name of said affected object (either the table, user type, function, or aggregate name).', + name: 'cassandra.response.event.schema_change.object', + type: 'keyword', + }, + 'cassandra.response.event.schema_change.target': { + category: 'cassandra', + description: 'Target could be "FUNCTION" or "AGGREGATE", multiple arguments.', + name: 'cassandra.response.event.schema_change.target', + type: 'keyword', + }, + 'cassandra.response.event.schema_change.name': { + category: 'cassandra', + description: 'The function/aggregate name.', + name: 'cassandra.response.event.schema_change.name', + type: 'keyword', + }, + 'cassandra.response.event.schema_change.args': { + category: 'cassandra', + description: 'One string for each argument type (as CQL type).', + name: 'cassandra.response.event.schema_change.args', + type: 'keyword', + }, + 'cassandra.response.error.code': { + category: 'cassandra', + description: 'The error code of the Cassandra response.', + name: 'cassandra.response.error.code', + type: 'long', + }, + 'cassandra.response.error.msg': { + category: 'cassandra', + description: 'The error message of the Cassandra response.', + name: 'cassandra.response.error.msg', + type: 'keyword', + }, + 'cassandra.response.error.type': { + category: 'cassandra', + description: 'The error type of the Cassandra response.', + name: 'cassandra.response.error.type', + type: 'keyword', + }, + 'cassandra.response.error.details.read_consistency': { + category: 'cassandra', + description: 'Representing the consistency level of the query that triggered the exception.', + name: 'cassandra.response.error.details.read_consistency', + type: 'keyword', + }, + 'cassandra.response.error.details.required': { + category: 'cassandra', + description: + 'Representing the number of nodes that should be alive to respect consistency level.', + name: 'cassandra.response.error.details.required', + type: 'long', + }, + 'cassandra.response.error.details.alive': { + category: 'cassandra', + description: + 'Representing the number of replicas that were known to be alive when the request had been processed (since an unavailable exception has been triggered).', + name: 'cassandra.response.error.details.alive', + type: 'long', + }, + 'cassandra.response.error.details.received': { + category: 'cassandra', + description: 'Representing the number of nodes having acknowledged the request.', + name: 'cassandra.response.error.details.received', + type: 'long', + }, + 'cassandra.response.error.details.blockfor': { + category: 'cassandra', + description: + 'Representing the number of replicas whose acknowledgement is required to achieve consistency level.', + name: 'cassandra.response.error.details.blockfor', + type: 'long', + }, + 'cassandra.response.error.details.write_type': { + category: 'cassandra', + description: 'Describe the type of the write that timed out.', + name: 'cassandra.response.error.details.write_type', + type: 'keyword', + }, + 'cassandra.response.error.details.data_present': { + category: 'cassandra', + description: 'It means the replica that was asked for data had responded.', + name: 'cassandra.response.error.details.data_present', + type: 'boolean', + }, + 'cassandra.response.error.details.keyspace': { + category: 'cassandra', + description: 'The keyspace of the failed function.', + name: 'cassandra.response.error.details.keyspace', + type: 'keyword', + }, + 'cassandra.response.error.details.table': { + category: 'cassandra', + description: 'The keyspace of the failed function.', + name: 'cassandra.response.error.details.table', + type: 'keyword', + }, + 'cassandra.response.error.details.stmt_id': { + category: 'cassandra', + description: 'Representing the unknown ID.', + name: 'cassandra.response.error.details.stmt_id', + type: 'keyword', + }, + 'cassandra.response.error.details.num_failures': { + category: 'cassandra', + description: + 'Representing the number of nodes that experience a failure while executing the request.', + name: 'cassandra.response.error.details.num_failures', + type: 'keyword', + }, + 'cassandra.response.error.details.function': { + category: 'cassandra', + description: 'The name of the failed function.', + name: 'cassandra.response.error.details.function', + type: 'keyword', + }, + 'cassandra.response.error.details.arg_types': { + category: 'cassandra', + description: 'One string for each argument type (as CQL type) of the failed function.', + name: 'cassandra.response.error.details.arg_types', + type: 'keyword', + }, + 'dhcpv4.transaction_id': { + category: 'dhcpv4', + description: + 'Transaction ID, a random number chosen by the client, used by the client and server to associate messages and responses between a client and a server. ', + name: 'dhcpv4.transaction_id', + type: 'keyword', + }, + 'dhcpv4.seconds': { + category: 'dhcpv4', + description: + 'Number of seconds elapsed since client began address acquisition or renewal process. ', + name: 'dhcpv4.seconds', + type: 'long', + }, + 'dhcpv4.flags': { + category: 'dhcpv4', + description: + 'Flags are set by the client to indicate how the DHCP server should its reply -- either unicast or broadcast. ', + name: 'dhcpv4.flags', + type: 'keyword', + }, + 'dhcpv4.client_ip': { + category: 'dhcpv4', + description: 'The current IP address of the client.', + name: 'dhcpv4.client_ip', + type: 'ip', + }, + 'dhcpv4.assigned_ip': { + category: 'dhcpv4', + description: + 'The IP address that the DHCP server is assigning to the client. This field is also known as "your" IP address. ', + name: 'dhcpv4.assigned_ip', + type: 'ip', + }, + 'dhcpv4.server_ip': { + category: 'dhcpv4', + description: + 'The IP address of the DHCP server that the client should use for the next step in the bootstrap process. ', + name: 'dhcpv4.server_ip', + type: 'ip', + }, + 'dhcpv4.relay_ip': { + category: 'dhcpv4', + description: + 'The relay IP address used by the client to contact the server (i.e. a DHCP relay server). ', + name: 'dhcpv4.relay_ip', + type: 'ip', + }, + 'dhcpv4.client_mac': { + category: 'dhcpv4', + description: "The client's MAC address (layer two).", + name: 'dhcpv4.client_mac', + type: 'keyword', + }, + 'dhcpv4.server_name': { + category: 'dhcpv4', + description: + 'The name of the server sending the message. Optional. Used in DHCPOFFER or DHCPACK messages. ', + name: 'dhcpv4.server_name', + type: 'keyword', + }, + 'dhcpv4.op_code': { + category: 'dhcpv4', + description: 'The message op code (bootrequest or bootreply). ', + example: 'bootreply', + name: 'dhcpv4.op_code', + type: 'keyword', + }, + 'dhcpv4.hops': { + category: 'dhcpv4', + description: 'The number of hops the DHCP message went through.', + name: 'dhcpv4.hops', + type: 'long', + }, + 'dhcpv4.hardware_type': { + category: 'dhcpv4', + description: 'The type of hardware used for the local network (Ethernet, LocalTalk, etc). ', + name: 'dhcpv4.hardware_type', + type: 'keyword', + }, + 'dhcpv4.option.message_type': { + category: 'dhcpv4', + description: + 'The specific type of DHCP message being sent (e.g. discover, offer, request, decline, ack, nak, release, inform). ', + example: 'ack', + name: 'dhcpv4.option.message_type', + type: 'keyword', + }, + 'dhcpv4.option.parameter_request_list': { + category: 'dhcpv4', + description: + 'This option is used by a DHCP client to request values for specified configuration parameters. ', + name: 'dhcpv4.option.parameter_request_list', + type: 'keyword', + }, + 'dhcpv4.option.requested_ip_address': { + category: 'dhcpv4', + description: + 'This option is used in a client request (DHCPDISCOVER) to allow the client to request that a particular IP address be assigned. ', + name: 'dhcpv4.option.requested_ip_address', + type: 'ip', + }, + 'dhcpv4.option.server_identifier': { + category: 'dhcpv4', + description: 'IP address of the individual DHCP server which handled this message. ', + name: 'dhcpv4.option.server_identifier', + type: 'ip', + }, + 'dhcpv4.option.broadcast_address': { + category: 'dhcpv4', + description: "This option specifies the broadcast address in use on the client's subnet. ", + name: 'dhcpv4.option.broadcast_address', + type: 'ip', + }, + 'dhcpv4.option.max_dhcp_message_size': { + category: 'dhcpv4', + description: + 'This option specifies the maximum length DHCP message that the client is willing to accept. ', + name: 'dhcpv4.option.max_dhcp_message_size', + type: 'long', + }, + 'dhcpv4.option.class_identifier': { + category: 'dhcpv4', + description: + "This option is used by DHCP clients to optionally identify the vendor type and configuration of a DHCP client. Vendors may choose to define specific vendor class identifiers to convey particular configuration or other identification information about a client. For example, the identifier may encode the client's hardware configuration. ", + name: 'dhcpv4.option.class_identifier', + type: 'keyword', + }, + 'dhcpv4.option.domain_name': { + category: 'dhcpv4', + description: + 'This option specifies the domain name that client should use when resolving hostnames via the Domain Name System. ', + name: 'dhcpv4.option.domain_name', + type: 'keyword', + }, + 'dhcpv4.option.dns_servers': { + category: 'dhcpv4', + description: + 'The domain name server option specifies a list of Domain Name System servers available to the client. ', + name: 'dhcpv4.option.dns_servers', + type: 'ip', + }, + 'dhcpv4.option.vendor_identifying_options': { + category: 'dhcpv4', + description: + 'A DHCP client may use this option to unambiguously identify the vendor that manufactured the hardware on which the client is running, the software in use, or an industry consortium to which the vendor belongs. This field is described in RFC 3925. ', + name: 'dhcpv4.option.vendor_identifying_options', + type: 'object', + }, + 'dhcpv4.option.subnet_mask': { + category: 'dhcpv4', + description: 'The subnet mask that the client should use on the currnet network. ', + name: 'dhcpv4.option.subnet_mask', + type: 'ip', + }, + 'dhcpv4.option.utc_time_offset_sec': { + category: 'dhcpv4', + description: + "The time offset field specifies the offset of the client's subnet in seconds from Coordinated Universal Time (UTC). ", + name: 'dhcpv4.option.utc_time_offset_sec', + type: 'long', + }, + 'dhcpv4.option.router': { + category: 'dhcpv4', + description: + "The router option specifies a list of IP addresses for routers on the client's subnet. ", + name: 'dhcpv4.option.router', + type: 'ip', + }, + 'dhcpv4.option.time_servers': { + category: 'dhcpv4', + description: + 'The time server option specifies a list of RFC 868 time servers available to the client. ', + name: 'dhcpv4.option.time_servers', + type: 'ip', + }, + 'dhcpv4.option.ntp_servers': { + category: 'dhcpv4', + description: + 'This option specifies a list of IP addresses indicating NTP servers available to the client. ', + name: 'dhcpv4.option.ntp_servers', + type: 'ip', + }, + 'dhcpv4.option.hostname': { + category: 'dhcpv4', + description: 'This option specifies the name of the client. ', + name: 'dhcpv4.option.hostname', + type: 'keyword', + }, + 'dhcpv4.option.ip_address_lease_time_sec': { + category: 'dhcpv4', + description: + 'This option is used in a client request (DHCPDISCOVER or DHCPREQUEST) to allow the client to request a lease time for the IP address. In a server reply (DHCPOFFER), a DHCP server uses this option to specify the lease time it is willing to offer. ', + name: 'dhcpv4.option.ip_address_lease_time_sec', + type: 'long', + }, + 'dhcpv4.option.message': { + category: 'dhcpv4', + description: + 'This option is used by a DHCP server to provide an error message to a DHCP client in a DHCPNAK message in the event of a failure. A client may use this option in a DHCPDECLINE message to indicate the why the client declined the offered parameters. ', + name: 'dhcpv4.option.message', + type: 'text', + }, + 'dhcpv4.option.renewal_time_sec': { + category: 'dhcpv4', + description: + 'This option specifies the time interval from address assignment until the client transitions to the RENEWING state. ', + name: 'dhcpv4.option.renewal_time_sec', + type: 'long', + }, + 'dhcpv4.option.rebinding_time_sec': { + category: 'dhcpv4', + description: + 'This option specifies the time interval from address assignment until the client transitions to the REBINDING state. ', + name: 'dhcpv4.option.rebinding_time_sec', + type: 'long', + }, + 'dhcpv4.option.boot_file_name': { + category: 'dhcpv4', + description: + "This option is used to identify a bootfile when the 'file' field in the DHCP header has been used for DHCP options. ", + name: 'dhcpv4.option.boot_file_name', + type: 'keyword', + }, + 'dns.flags.authoritative': { + category: 'dns', + description: + 'A DNS flag specifying that the responding server is an authority for the domain name used in the question. ', + name: 'dns.flags.authoritative', + type: 'boolean', + }, + 'dns.flags.recursion_available': { + category: 'dns', + description: + 'A DNS flag specifying whether recursive query support is available in the name server. ', + name: 'dns.flags.recursion_available', + type: 'boolean', + }, + 'dns.flags.recursion_desired': { + category: 'dns', + description: + 'A DNS flag specifying that the client directs the server to pursue a query recursively. Recursive query support is optional. ', + name: 'dns.flags.recursion_desired', + type: 'boolean', + }, + 'dns.flags.authentic_data': { + category: 'dns', + description: + 'A DNS flag specifying that the recursive server considers the response authentic. ', + name: 'dns.flags.authentic_data', + type: 'boolean', + }, + 'dns.flags.checking_disabled': { + category: 'dns', + description: + 'A DNS flag specifying that the client disables the server signature validation of the query. ', + name: 'dns.flags.checking_disabled', + type: 'boolean', + }, + 'dns.flags.truncated_response': { + category: 'dns', + description: 'A DNS flag specifying that only the first 512 bytes of the reply were returned. ', + name: 'dns.flags.truncated_response', + type: 'boolean', + }, + 'dns.question.etld_plus_one': { + category: 'dns', + description: + 'The effective top-level domain (eTLD) plus one more label. For example, the eTLD+1 for "foo.bar.golang.org." is "golang.org.". The data for determining the eTLD comes from an embedded copy of the data from http://publicsuffix.org.', + example: 'amazon.co.uk.', + name: 'dns.question.etld_plus_one', + }, + 'dns.answers_count': { + category: 'dns', + description: 'The number of resource records contained in the `dns.answers` field. ', + name: 'dns.answers_count', + type: 'long', + }, + 'dns.authorities': { + category: 'dns', + description: 'An array containing a dictionary for each authority section from the answer. ', + name: 'dns.authorities', + type: 'object', + }, + 'dns.authorities_count': { + category: 'dns', + description: + 'The number of resource records contained in the `dns.authorities` field. The `dns.authorities` field may or may not be included depending on the configuration of Packetbeat. ', + name: 'dns.authorities_count', + type: 'long', + }, + 'dns.authorities.name': { + category: 'dns', + description: 'The domain name to which this resource record pertains.', + example: 'example.com.', + name: 'dns.authorities.name', + }, + 'dns.authorities.type': { + category: 'dns', + description: 'The type of data contained in this resource record.', + example: 'NS', + name: 'dns.authorities.type', + }, + 'dns.authorities.class': { + category: 'dns', + description: 'The class of DNS data contained in this resource record.', + example: 'IN', + name: 'dns.authorities.class', + }, + 'dns.additionals': { + category: 'dns', + description: 'An array containing a dictionary for each additional section from the answer. ', + name: 'dns.additionals', + type: 'object', + }, + 'dns.additionals_count': { + category: 'dns', + description: + 'The number of resource records contained in the `dns.additionals` field. The `dns.additionals` field may or may not be included depending on the configuration of Packetbeat. ', + name: 'dns.additionals_count', + type: 'long', + }, + 'dns.additionals.name': { + category: 'dns', + description: 'The domain name to which this resource record pertains.', + example: 'example.com.', + name: 'dns.additionals.name', + }, + 'dns.additionals.type': { + category: 'dns', + description: 'The type of data contained in this resource record.', + example: 'NS', + name: 'dns.additionals.type', + }, + 'dns.additionals.class': { + category: 'dns', + description: 'The class of DNS data contained in this resource record.', + example: 'IN', + name: 'dns.additionals.class', + }, + 'dns.additionals.ttl': { + category: 'dns', + description: + 'The time interval in seconds that this resource record may be cached before it should be discarded. Zero values mean that the data should not be cached. ', + name: 'dns.additionals.ttl', + type: 'long', + }, + 'dns.additionals.data': { + category: 'dns', + description: + 'The data describing the resource. The meaning of this data depends on the type and class of the resource record. ', + name: 'dns.additionals.data', + }, + 'dns.opt.version': { + category: 'dns', + description: 'The EDNS version.', + example: '0', + name: 'dns.opt.version', + }, + 'dns.opt.do': { + category: 'dns', + description: 'If set, the transaction uses DNSSEC.', + name: 'dns.opt.do', + type: 'boolean', + }, + 'dns.opt.ext_rcode': { + category: 'dns', + description: 'Extended response code field.', + example: 'BADVERS', + name: 'dns.opt.ext_rcode', + }, + 'dns.opt.udp_size': { + category: 'dns', + description: "Requestor's UDP payload size (in bytes).", + name: 'dns.opt.udp_size', + type: 'long', + }, + 'http.request.headers': { + category: 'http', + description: + 'A map containing the captured header fields from the request. Which headers to capture is configurable. If headers with the same header name are present in the message, they will be separated by commas. ', + name: 'http.request.headers', + type: 'object', + }, + 'http.request.params': { + category: 'http', + name: 'http.request.params', + type: 'alias', + }, + 'http.response.status_phrase': { + category: 'http', + description: 'The HTTP status phrase.', + example: 'Not Found', + name: 'http.response.status_phrase', + }, + 'http.response.headers': { + category: 'http', + description: + 'A map containing the captured header fields from the response. Which headers to capture is configurable. If headers with the same header name are present in the message, they will be separated by commas. ', + name: 'http.response.headers', + type: 'object', + }, + 'http.response.code': { + category: 'http', + name: 'http.response.code', + type: 'alias', + }, + 'http.response.phrase': { + category: 'http', + name: 'http.response.phrase', + type: 'alias', + }, + 'icmp.version': { + category: 'icmp', + description: 'The version of the ICMP protocol.', + name: 'icmp.version', + }, + 'icmp.request.message': { + category: 'icmp', + description: 'A human readable form of the request.', + name: 'icmp.request.message', + type: 'keyword', + }, + 'icmp.request.type': { + category: 'icmp', + description: 'The request type.', + name: 'icmp.request.type', + type: 'long', + }, + 'icmp.request.code': { + category: 'icmp', + description: 'The request code.', + name: 'icmp.request.code', + type: 'long', + }, + 'icmp.response.message': { + category: 'icmp', + description: 'A human readable form of the response.', + name: 'icmp.response.message', + type: 'keyword', + }, + 'icmp.response.type': { + category: 'icmp', + description: 'The response type.', + name: 'icmp.response.type', + type: 'long', + }, + 'icmp.response.code': { + category: 'icmp', + description: 'The response code.', + name: 'icmp.response.code', + type: 'long', + }, + 'memcache.protocol_type': { + category: 'memcache', + description: + 'The memcache protocol implementation. The value can be "binary" for binary-based, "text" for text-based, or "unknown" for an unknown memcache protocol type. ', + name: 'memcache.protocol_type', + type: 'keyword', + }, + 'memcache.request.line': { + category: 'memcache', + description: 'The raw command line for unknown commands ONLY. ', + name: 'memcache.request.line', + type: 'keyword', + }, + 'memcache.request.command': { + category: 'memcache', + description: + 'The memcache command being requested in the memcache text protocol. For example "set" or "get". The binary protocol opcodes are translated into memcache text protocol commands. ', + name: 'memcache.request.command', + type: 'keyword', + }, + 'memcache.response.command': { + category: 'memcache', + description: + 'Either the text based protocol response message type or the name of the originating request if binary protocol is used. ', + name: 'memcache.response.command', + type: 'keyword', + }, + 'memcache.request.type': { + category: 'memcache', + description: + 'The memcache command classification. This value can be "UNKNOWN", "Load", "Store", "Delete", "Counter", "Info", "SlabCtrl", "LRUCrawler", "Stats", "Success", "Fail", or "Auth". ', + name: 'memcache.request.type', + type: 'keyword', + }, + 'memcache.response.type': { + category: 'memcache', + description: + 'The memcache command classification. This value can be "UNKNOWN", "Load", "Store", "Delete", "Counter", "Info", "SlabCtrl", "LRUCrawler", "Stats", "Success", "Fail", or "Auth". The text based protocol will employ any of these, whereas the binary based protocol will mirror the request commands only (see `memcache.response.status` for binary protocol). ', + name: 'memcache.response.type', + type: 'keyword', + }, + 'memcache.response.error_msg': { + category: 'memcache', + description: 'The optional error message in the memcache response (text based protocol only). ', + name: 'memcache.response.error_msg', + type: 'keyword', + }, + 'memcache.request.opcode': { + category: 'memcache', + description: 'The binary protocol message opcode name. ', + name: 'memcache.request.opcode', + type: 'keyword', + }, + 'memcache.response.opcode': { + category: 'memcache', + description: 'The binary protocol message opcode name. ', + name: 'memcache.response.opcode', + type: 'keyword', + }, + 'memcache.request.opcode_value': { + category: 'memcache', + description: 'The binary protocol message opcode value. ', + name: 'memcache.request.opcode_value', + type: 'long', + }, + 'memcache.response.opcode_value': { + category: 'memcache', + description: 'The binary protocol message opcode value. ', + name: 'memcache.response.opcode_value', + type: 'long', + }, + 'memcache.request.opaque': { + category: 'memcache', + description: + 'The binary protocol opaque header value used for correlating request with response messages. ', + name: 'memcache.request.opaque', + type: 'long', + }, + 'memcache.response.opaque': { + category: 'memcache', + description: + 'The binary protocol opaque header value used for correlating request with response messages. ', + name: 'memcache.response.opaque', + type: 'long', + }, + 'memcache.request.vbucket': { + category: 'memcache', + description: 'The vbucket index sent in the binary message. ', + name: 'memcache.request.vbucket', + type: 'long', + }, + 'memcache.response.status': { + category: 'memcache', + description: 'The textual representation of the response error code (binary protocol only). ', + name: 'memcache.response.status', + type: 'keyword', + }, + 'memcache.response.status_code': { + category: 'memcache', + description: 'The status code value returned in the response (binary protocol only). ', + name: 'memcache.response.status_code', + type: 'long', + }, + 'memcache.request.keys': { + category: 'memcache', + description: 'The list of keys sent in the store or load commands. ', + name: 'memcache.request.keys', + type: 'array', + }, + 'memcache.response.keys': { + category: 'memcache', + description: 'The list of keys returned for the load command (if present). ', + name: 'memcache.response.keys', + type: 'array', + }, + 'memcache.request.count_values': { + category: 'memcache', + description: + 'The number of values found in the memcache request message. If the command does not send any data, this field is missing. ', + name: 'memcache.request.count_values', + type: 'long', + }, + 'memcache.response.count_values': { + category: 'memcache', + description: + 'The number of values found in the memcache response message. If the command does not send any data, this field is missing. ', + name: 'memcache.response.count_values', + type: 'long', + }, + 'memcache.request.values': { + category: 'memcache', + description: 'The list of base64 encoded values sent with the request (if present). ', + name: 'memcache.request.values', + type: 'array', + }, + 'memcache.response.values': { + category: 'memcache', + description: 'The list of base64 encoded values sent with the response (if present). ', + name: 'memcache.response.values', + type: 'array', + }, + 'memcache.request.bytes': { + category: 'memcache', + description: 'The byte count of the values being transferred. ', + name: 'memcache.request.bytes', + type: 'long', + format: 'bytes', + }, + 'memcache.response.bytes': { + category: 'memcache', + description: 'The byte count of the values being transferred. ', + name: 'memcache.response.bytes', + type: 'long', + format: 'bytes', + }, + 'memcache.request.delta': { + category: 'memcache', + description: 'The counter increment/decrement delta value. ', + name: 'memcache.request.delta', + type: 'long', + }, + 'memcache.request.initial': { + category: 'memcache', + description: 'The counter increment/decrement initial value parameter (binary protocol only). ', + name: 'memcache.request.initial', + type: 'long', + }, + 'memcache.request.verbosity': { + category: 'memcache', + description: 'The value of the memcache "verbosity" command. ', + name: 'memcache.request.verbosity', + type: 'long', + }, + 'memcache.request.raw_args': { + category: 'memcache', + description: + 'The text protocol raw arguments for the "stats ..." and "lru crawl ..." commands. ', + name: 'memcache.request.raw_args', + type: 'keyword', + }, + 'memcache.request.source_class': { + category: 'memcache', + description: "The source class id in 'slab reassign' command. ", + name: 'memcache.request.source_class', + type: 'long', + }, + 'memcache.request.dest_class': { + category: 'memcache', + description: "The destination class id in 'slab reassign' command. ", + name: 'memcache.request.dest_class', + type: 'long', + }, + 'memcache.request.automove': { + category: 'memcache', + description: + 'The automove mode in the \'slab automove\' command expressed as a string. This value can be "standby"(=0), "slow"(=1), "aggressive"(=2), or the raw value if the value is unknown. ', + name: 'memcache.request.automove', + type: 'keyword', + }, + 'memcache.request.flags': { + category: 'memcache', + description: 'The memcache command flags sent in the request (if present). ', + name: 'memcache.request.flags', + type: 'long', + }, + 'memcache.response.flags': { + category: 'memcache', + description: 'The memcache message flags sent in the response (if present). ', + name: 'memcache.response.flags', + type: 'long', + }, + 'memcache.request.exptime': { + category: 'memcache', + description: + 'The data expiry time in seconds sent with the memcache command (if present). If the value is <30 days, the expiry time is relative to "now", or else it is an absolute Unix time in seconds (32-bit). ', + name: 'memcache.request.exptime', + type: 'long', + }, + 'memcache.request.sleep_us': { + category: 'memcache', + description: "The sleep setting in microseconds for the 'lru_crawler sleep' command. ", + name: 'memcache.request.sleep_us', + type: 'long', + }, + 'memcache.response.value': { + category: 'memcache', + description: 'The counter value returned by a counter operation. ', + name: 'memcache.response.value', + type: 'long', + }, + 'memcache.request.noreply': { + category: 'memcache', + description: + 'Set to true if noreply was set in the request. The `memcache.response` field will be missing. ', + name: 'memcache.request.noreply', + type: 'boolean', + }, + 'memcache.request.quiet': { + category: 'memcache', + description: 'Set to true if the binary protocol message is to be treated as a quiet message. ', + name: 'memcache.request.quiet', + type: 'boolean', + }, + 'memcache.request.cas_unique': { + category: 'memcache', + description: 'The CAS (compare-and-swap) identifier if present. ', + name: 'memcache.request.cas_unique', + type: 'long', + }, + 'memcache.response.cas_unique': { + category: 'memcache', + description: + 'The CAS (compare-and-swap) identifier to be used with CAS-based updates (if present). ', + name: 'memcache.response.cas_unique', + type: 'long', + }, + 'memcache.response.stats': { + category: 'memcache', + description: + 'The list of statistic values returned. Each entry is a dictionary with the fields "name" and "value". ', + name: 'memcache.response.stats', + type: 'array', + }, + 'memcache.response.version': { + category: 'memcache', + description: 'The returned memcache version string. ', + name: 'memcache.response.version', + type: 'keyword', + }, + 'mongodb.error': { + category: 'mongodb', + description: + 'If the MongoDB request has resulted in an error, this field contains the error message returned by the server. ', + name: 'mongodb.error', + }, + 'mongodb.fullCollectionName': { + category: 'mongodb', + description: + 'The full collection name. The full collection name is the concatenation of the database name with the collection name, using a dot (.) for the concatenation. For example, for the database foo and the collection bar, the full collection name is foo.bar. ', + name: 'mongodb.fullCollectionName', + }, + 'mongodb.numberToSkip': { + category: 'mongodb', + description: + 'Sets the number of documents to omit - starting from the first document in the resulting dataset - when returning the result of the query. ', + name: 'mongodb.numberToSkip', + type: 'long', + }, + 'mongodb.numberToReturn': { + category: 'mongodb', + description: 'The requested maximum number of documents to be returned. ', + name: 'mongodb.numberToReturn', + type: 'long', + }, + 'mongodb.numberReturned': { + category: 'mongodb', + description: 'The number of documents in the reply. ', + name: 'mongodb.numberReturned', + type: 'long', + }, + 'mongodb.startingFrom': { + category: 'mongodb', + description: 'Where in the cursor this reply is starting. ', + name: 'mongodb.startingFrom', + }, + 'mongodb.query': { + category: 'mongodb', + description: + 'A JSON document that represents the query. The query will contain one or more elements, all of which must match for a document to be included in the result set. Possible elements include $query, $orderby, $hint, $explain, and $snapshot. ', + name: 'mongodb.query', + }, + 'mongodb.returnFieldsSelector': { + category: 'mongodb', + description: + 'A JSON document that limits the fields in the returned documents. The returnFieldsSelector contains one or more elements, each of which is the name of a field that should be returned, and the integer value 1. ', + name: 'mongodb.returnFieldsSelector', + }, + 'mongodb.selector': { + category: 'mongodb', + description: + 'A BSON document that specifies the query for selecting the document to update or delete. ', + name: 'mongodb.selector', + }, + 'mongodb.update': { + category: 'mongodb', + description: + 'A BSON document that specifies the update to be performed. For information on specifying updates, see the Update Operations documentation from the MongoDB Manual. ', + name: 'mongodb.update', + }, + 'mongodb.cursorId': { + category: 'mongodb', + description: + 'The cursor identifier returned in the OP_REPLY. This must be the value that was returned from the database. ', + name: 'mongodb.cursorId', + }, + 'mysql.affected_rows': { + category: 'mysql', + description: + 'If the MySQL command is successful, this field contains the affected number of rows of the last statement. ', + name: 'mysql.affected_rows', + type: 'long', + }, + 'mysql.insert_id': { + category: 'mysql', + description: + 'If the INSERT query is successful, this field contains the id of the newly inserted row. ', + name: 'mysql.insert_id', + }, + 'mysql.num_fields': { + category: 'mysql', + description: + 'If the SELECT query is successful, this field is set to the number of fields returned. ', + name: 'mysql.num_fields', + }, + 'mysql.num_rows': { + category: 'mysql', + description: + 'If the SELECT query is successful, this field is set to the number of rows returned. ', + name: 'mysql.num_rows', + }, + 'mysql.query': { + category: 'mysql', + description: "The row mysql query as read from the transaction's request. ", + name: 'mysql.query', + }, + 'mysql.error_code': { + category: 'mysql', + description: 'The error code returned by MySQL. ', + name: 'mysql.error_code', + type: 'long', + }, + 'mysql.error_message': { + category: 'mysql', + description: 'The error info message returned by MySQL. ', + name: 'mysql.error_message', + }, + 'nfs.version': { + category: 'nfs', + description: 'NFS protocol version number.', + name: 'nfs.version', + type: 'long', + }, + 'nfs.minor_version': { + category: 'nfs', + description: 'NFS protocol minor version number.', + name: 'nfs.minor_version', + type: 'long', + }, + 'nfs.tag': { + category: 'nfs', + description: 'NFS v4 COMPOUND operation tag.', + name: 'nfs.tag', + }, + 'nfs.opcode': { + category: 'nfs', + description: 'NFS operation name, or main operation name, in case of COMPOUND calls. ', + name: 'nfs.opcode', + }, + 'nfs.status': { + category: 'nfs', + description: 'NFS operation reply status.', + name: 'nfs.status', + }, + 'rpc.xid': { + category: 'rpc', + description: 'RPC message transaction identifier.', + name: 'rpc.xid', + }, + 'rpc.status': { + category: 'rpc', + description: 'RPC message reply status.', + name: 'rpc.status', + }, + 'rpc.auth_flavor': { + category: 'rpc', + description: 'RPC authentication flavor.', + name: 'rpc.auth_flavor', + }, + 'rpc.cred.uid': { + category: 'rpc', + description: "RPC caller's user id, in case of auth-unix.", + name: 'rpc.cred.uid', + type: 'long', + }, + 'rpc.cred.gid': { + category: 'rpc', + description: "RPC caller's group id, in case of auth-unix.", + name: 'rpc.cred.gid', + type: 'long', + }, + 'rpc.cred.gids': { + category: 'rpc', + description: "RPC caller's secondary group ids, in case of auth-unix.", + name: 'rpc.cred.gids', + }, + 'rpc.cred.stamp': { + category: 'rpc', + description: 'Arbitrary ID which the caller machine may generate.', + name: 'rpc.cred.stamp', + type: 'long', + }, + 'rpc.cred.machinename': { + category: 'rpc', + description: "The name of the caller's machine.", + name: 'rpc.cred.machinename', + }, + 'rpc.call_size': { + category: 'rpc', + description: 'RPC call size with argument.', + name: 'rpc.call_size', + type: 'alias', + }, + 'rpc.reply_size': { + category: 'rpc', + description: 'RPC reply size with argument.', + name: 'rpc.reply_size', + type: 'alias', + }, + 'pgsql.error_code': { + category: 'pgsql', + description: 'The PostgreSQL error code.', + name: 'pgsql.error_code', + type: 'long', + }, + 'pgsql.error_message': { + category: 'pgsql', + description: 'The PostgreSQL error message.', + name: 'pgsql.error_message', + }, + 'pgsql.error_severity': { + category: 'pgsql', + description: 'The PostgreSQL error severity.', + name: 'pgsql.error_severity', + }, + 'pgsql.num_fields': { + category: 'pgsql', + description: + 'If the SELECT query if successful, this field is set to the number of fields returned. ', + name: 'pgsql.num_fields', + }, + 'pgsql.num_rows': { + category: 'pgsql', + description: + 'If the SELECT query if successful, this field is set to the number of rows returned. ', + name: 'pgsql.num_rows', + }, + 'redis.return_value': { + category: 'redis', + description: 'The return value of the Redis command in a human readable format. ', + name: 'redis.return_value', + }, + 'redis.error': { + category: 'redis', + description: + 'If the Redis command has resulted in an error, this field contains the error message returned by the Redis server. ', + name: 'redis.error', + }, + 'thrift.params': { + category: 'thrift', + description: + 'The RPC method call parameters in a human readable format. If the IDL files are available, the parameters use names whenever possible. Otherwise, the IDs from the message are used. ', + name: 'thrift.params', + }, + 'thrift.service': { + category: 'thrift', + description: 'The name of the Thrift-RPC service as defined in the IDL files. ', + name: 'thrift.service', + }, + 'thrift.return_value': { + category: 'thrift', + description: + 'The value returned by the Thrift-RPC call. This is encoded in a human readable format. ', + name: 'thrift.return_value', + }, + 'thrift.exceptions': { + category: 'thrift', + description: + 'If the call resulted in exceptions, this field contains the exceptions in a human readable format. ', + name: 'thrift.exceptions', + }, + 'tls.client.x509.version': { + category: 'tls', + description: 'Version of x509 format.', + example: 3, + name: 'tls.client.x509.version', + type: 'keyword', + }, + 'tls.client.x509.version_number': { + category: 'tls', + description: 'Version of x509 format.', + example: 3, + name: 'tls.client.x509.version_number', + type: 'keyword', + }, + 'tls.client.x509.serial_number': { + category: 'tls', + description: + 'Unique serial number issued by the certificate authority. For consistency, if this value is alphanumeric, it should be formatted without colons and uppercase characters. ', + example: '55FBB9C7DEBF09809D12CCAA', + name: 'tls.client.x509.serial_number', + type: 'keyword', + }, + 'tls.client.x509.issuer.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of issuing certificate authority.', + example: 'C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert SHA2 High Assurance Server CA', + name: 'tls.client.x509.issuer.distinguished_name', + type: 'keyword', + }, + 'tls.client.x509.issuer.common_name': { + category: 'tls', + description: 'List of common name (CN) of issuing certificate authority.', + example: 'DigiCert SHA2 High Assurance Server CA', + name: 'tls.client.x509.issuer.common_name', + type: 'keyword', + }, + 'tls.client.x509.issuer.organizational_unit': { + category: 'tls', + description: 'List of organizational units (OU) of issuing certificate authority.', + example: 'www.digicert.com', + name: 'tls.client.x509.issuer.organizational_unit', + type: 'keyword', + }, + 'tls.client.x509.issuer.organization': { + category: 'tls', + description: 'List of organizations (O) of issuing certificate authority.', + example: 'DigiCert Inc', + name: 'tls.client.x509.issuer.organization', + type: 'keyword', + }, + 'tls.client.x509.issuer.locality': { + category: 'tls', + description: 'List of locality names (L)', + example: 'Mountain View', + name: 'tls.client.x509.issuer.locality', + type: 'keyword', + }, + 'tls.client.x509.issuer.province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.client.x509.issuer.province', + type: 'keyword', + }, + 'tls.client.x509.issuer.state_or_province': { + category: 'tls', + description: 'List of state or province names (ST, S, or P)', + example: 'California', + name: 'tls.client.x509.issuer.state_or_province', + type: 'keyword', + }, + 'tls.client.x509.issuer.country': { + category: 'tls', + description: 'List of country (C) codes', + example: 'US', + name: 'tls.client.x509.issuer.country', + type: 'keyword', + }, + 'tls.client.x509.signature_algorithm': { + category: 'tls', + description: + 'Identifier for certificate signature algorithm. Recommend using names found in Go Lang Crypto library (See https://github.com/golang/go/blob/go1.14/src/crypto/x509/x509.go#L337-L353).', + example: 'SHA256-RSA', + name: 'tls.client.x509.signature_algorithm', + type: 'keyword', + }, + 'tls.client.x509.not_before': { + category: 'tls', + description: 'Time at which the certificate is first considered valid.', + example: '"2019-08-16T01:40:25.000Z"', + name: 'tls.client.x509.not_before', + type: 'date', + }, + 'tls.client.x509.not_after': { + category: 'tls', + description: 'Time at which the certificate is no longer considered valid.', + example: '"2020-07-16T03:15:39.000Z"', + name: 'tls.client.x509.not_after', + type: 'date', + }, + 'tls.client.x509.subject.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of the certificate subject entity.', + example: 'C=US, ST=California, L=San Francisco, O=Fastly, Inc., CN=r2.shared.global.fastly.net', + name: 'tls.client.x509.subject.distinguished_name', + type: 'keyword', + }, + 'tls.client.x509.subject.common_name': { + category: 'tls', + description: 'List of common names (CN) of subject.', + example: 'r2.shared.global.fastly.net', + name: 'tls.client.x509.subject.common_name', + type: 'keyword', + }, + 'tls.client.x509.subject.organizational_unit': { + category: 'tls', + description: 'List of organizational units (OU) of subject.', + name: 'tls.client.x509.subject.organizational_unit', + type: 'keyword', + }, + 'tls.client.x509.subject.organization': { + category: 'tls', + description: 'List of organizations (O) of subject.', + example: 'Fastly, Inc.', + name: 'tls.client.x509.subject.organization', + type: 'keyword', + }, + 'tls.client.x509.subject.locality': { + category: 'tls', + description: 'List of locality names (L)', + example: 'San Francisco', + name: 'tls.client.x509.subject.locality', + type: 'keyword', + }, + 'tls.client.x509.subject.province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.client.x509.subject.province', + type: 'keyword', + }, + 'tls.client.x509.subject.state_or_province': { + category: 'tls', + description: 'List of state or province names (ST, S, or P)', + example: 'California', + name: 'tls.client.x509.subject.state_or_province', + type: 'keyword', + }, + 'tls.client.x509.subject.country': { + category: 'tls', + description: 'List of country (C) code', + example: 'US', + name: 'tls.client.x509.subject.country', + type: 'keyword', + }, + 'tls.client.x509.public_key_algorithm': { + category: 'tls', + description: 'Algorithm used to generate the public key.', + example: 'RSA', + name: 'tls.client.x509.public_key_algorithm', + type: 'keyword', + }, + 'tls.client.x509.public_key_size': { + category: 'tls', + description: 'The size of the public key space in bits.', + example: 2048, + name: 'tls.client.x509.public_key_size', + type: 'long', + }, + 'tls.client.x509.alternative_names': { + category: 'tls', + description: + 'List of subject alternative names (SAN). Name types vary by certificate authority and certificate type but commonly contain IP addresses, DNS names (and wildcards), and email addresses.', + example: '*.elastic.co', + name: 'tls.client.x509.alternative_names', + type: 'keyword', + }, + 'tls.server.x509.version': { + category: 'tls', + description: 'Version of x509 format.', + example: 3, + name: 'tls.server.x509.version', + type: 'keyword', + }, + 'tls.server.x509.version_number': { + category: 'tls', + description: 'Version of x509 format.', + example: 3, + name: 'tls.server.x509.version_number', + type: 'keyword', + }, + 'tls.server.x509.serial_number': { + category: 'tls', + description: + 'Unique serial number issued by the certificate authority. For consistency, if this value is alphanumeric, it should be formatted without colons and uppercase characters. ', + example: '55FBB9C7DEBF09809D12CCAA', + name: 'tls.server.x509.serial_number', + type: 'keyword', + }, + 'tls.server.x509.issuer.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of issuing certificate authority.', + example: 'C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert SHA2 High Assurance Server CA', + name: 'tls.server.x509.issuer.distinguished_name', + type: 'keyword', + }, + 'tls.server.x509.issuer.common_name': { + category: 'tls', + description: 'List of common name (CN) of issuing certificate authority.', + example: 'DigiCert SHA2 High Assurance Server CA', + name: 'tls.server.x509.issuer.common_name', + type: 'keyword', + }, + 'tls.server.x509.issuer.organizational_unit': { + category: 'tls', + description: 'List of organizational units (OU) of issuing certificate authority.', + example: 'www.digicert.com', + name: 'tls.server.x509.issuer.organizational_unit', + type: 'keyword', + }, + 'tls.server.x509.issuer.organization': { + category: 'tls', + description: 'List of organizations (O) of issuing certificate authority.', + example: 'DigiCert Inc', + name: 'tls.server.x509.issuer.organization', + type: 'keyword', + }, + 'tls.server.x509.issuer.locality': { + category: 'tls', + description: 'List of locality names (L)', + example: 'Mountain View', + name: 'tls.server.x509.issuer.locality', + type: 'keyword', + }, + 'tls.server.x509.issuer.province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.server.x509.issuer.province', + type: 'keyword', + }, + 'tls.server.x509.issuer.state_or_province': { + category: 'tls', + description: 'List of state or province names (ST, S, or P)', + example: 'California', + name: 'tls.server.x509.issuer.state_or_province', + type: 'keyword', + }, + 'tls.server.x509.issuer.country': { + category: 'tls', + description: 'List of country (C) codes', + example: 'US', + name: 'tls.server.x509.issuer.country', + type: 'keyword', + }, + 'tls.server.x509.signature_algorithm': { + category: 'tls', + description: + 'Identifier for certificate signature algorithm. Recommend using names found in Go Lang Crypto library (See https://github.com/golang/go/blob/go1.14/src/crypto/x509/x509.go#L337-L353).', + example: 'SHA256-RSA', + name: 'tls.server.x509.signature_algorithm', + type: 'keyword', + }, + 'tls.server.x509.not_before': { + category: 'tls', + description: 'Time at which the certificate is first considered valid.', + example: '"2019-08-16T01:40:25.000Z"', + name: 'tls.server.x509.not_before', + type: 'date', + }, + 'tls.server.x509.not_after': { + category: 'tls', + description: 'Time at which the certificate is no longer considered valid.', + example: '"2020-07-16T03:15:39.000Z"', + name: 'tls.server.x509.not_after', + type: 'date', + }, + 'tls.server.x509.subject.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of the certificate subject entity.', + example: 'C=US, ST=California, L=San Francisco, O=Fastly, Inc., CN=r2.shared.global.fastly.net', + name: 'tls.server.x509.subject.distinguished_name', + type: 'keyword', + }, + 'tls.server.x509.subject.common_name': { + category: 'tls', + description: 'List of common names (CN) of subject.', + example: 'r2.shared.global.fastly.net', + name: 'tls.server.x509.subject.common_name', + type: 'keyword', + }, + 'tls.server.x509.subject.organizational_unit': { + category: 'tls', + description: 'List of organizational units (OU) of subject.', + name: 'tls.server.x509.subject.organizational_unit', + type: 'keyword', + }, + 'tls.server.x509.subject.organization': { + category: 'tls', + description: 'List of organizations (O) of subject.', + example: 'Fastly, Inc.', + name: 'tls.server.x509.subject.organization', + type: 'keyword', + }, + 'tls.server.x509.subject.locality': { + category: 'tls', + description: 'List of locality names (L)', + example: 'San Francisco', + name: 'tls.server.x509.subject.locality', + type: 'keyword', + }, + 'tls.server.x509.subject.province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.server.x509.subject.province', + type: 'keyword', + }, + 'tls.server.x509.subject.state_or_province': { + category: 'tls', + description: 'List of state or province names (ST, S, or P)', + example: 'California', + name: 'tls.server.x509.subject.state_or_province', + type: 'keyword', + }, + 'tls.server.x509.subject.country': { + category: 'tls', + description: 'List of country (C) code', + example: 'US', + name: 'tls.server.x509.subject.country', + type: 'keyword', + }, + 'tls.server.x509.public_key_algorithm': { + category: 'tls', + description: 'Algorithm used to generate the public key.', + example: 'RSA', + name: 'tls.server.x509.public_key_algorithm', + type: 'keyword', + }, + 'tls.server.x509.public_key_size': { + category: 'tls', + description: 'The size of the public key space in bits.', + example: 2048, + name: 'tls.server.x509.public_key_size', + type: 'long', + }, + 'tls.server.x509.alternative_names': { + category: 'tls', + description: + 'List of subject alternative names (SAN). Name types vary by certificate authority and certificate type but commonly contain IP addresses, DNS names (and wildcards), and email addresses.', + example: '*.elastic.co', + name: 'tls.server.x509.alternative_names', + type: 'keyword', + }, + 'tls.detailed.version': { + category: 'tls', + description: 'The version of the TLS protocol used. ', + example: 'TLS 1.3', + name: 'tls.detailed.version', + type: 'keyword', + }, + 'tls.detailed.resumption_method': { + category: 'tls', + description: + 'If the session has been resumed, the underlying method used. One of "id" for TLS session ID or "ticket" for TLS ticket extension. ', + name: 'tls.detailed.resumption_method', + type: 'keyword', + }, + 'tls.detailed.client_certificate_requested': { + category: 'tls', + description: + 'Whether the server has requested the client to authenticate itself using a client certificate. ', + name: 'tls.detailed.client_certificate_requested', + type: 'boolean', + }, + 'tls.detailed.client_hello.version': { + category: 'tls', + description: + 'The version of the TLS protocol by which the client wishes to communicate during this session. ', + name: 'tls.detailed.client_hello.version', + type: 'keyword', + }, + 'tls.detailed.client_hello.session_id': { + category: 'tls', + description: + 'Unique number to identify the session for the corresponding connection with the client. ', + name: 'tls.detailed.client_hello.session_id', + type: 'keyword', + }, + 'tls.detailed.client_hello.supported_compression_methods': { + category: 'tls', + description: + 'The list of compression methods the client supports. See https://www.iana.org/assignments/comp-meth-ids/comp-meth-ids.xhtml ', + name: 'tls.detailed.client_hello.supported_compression_methods', + type: 'keyword', + }, + 'tls.detailed.client_hello.extensions.server_name_indication': { + category: 'tls', + description: 'List of hostnames', + name: 'tls.detailed.client_hello.extensions.server_name_indication', + type: 'keyword', + }, + 'tls.detailed.client_hello.extensions.application_layer_protocol_negotiation': { + category: 'tls', + description: 'List of application-layer protocols the client is willing to use. ', + name: 'tls.detailed.client_hello.extensions.application_layer_protocol_negotiation', + type: 'keyword', + }, + 'tls.detailed.client_hello.extensions.session_ticket': { + category: 'tls', + description: + 'Length of the session ticket, if provided, or an empty string to advertise support for tickets. ', + name: 'tls.detailed.client_hello.extensions.session_ticket', + type: 'keyword', + }, + 'tls.detailed.client_hello.extensions.supported_versions': { + category: 'tls', + description: 'List of TLS versions that the client is willing to use. ', + name: 'tls.detailed.client_hello.extensions.supported_versions', + type: 'keyword', + }, + 'tls.detailed.client_hello.extensions.supported_groups': { + category: 'tls', + description: 'List of Elliptic Curve Cryptography (ECC) curve groups supported by the client. ', + name: 'tls.detailed.client_hello.extensions.supported_groups', + type: 'keyword', + }, + 'tls.detailed.client_hello.extensions.signature_algorithms': { + category: 'tls', + description: 'List of signature algorithms that may be use in digital signatures. ', + name: 'tls.detailed.client_hello.extensions.signature_algorithms', + type: 'keyword', + }, + 'tls.detailed.client_hello.extensions.ec_points_formats': { + category: 'tls', + description: + 'List of Elliptic Curve (EC) point formats. Indicates the set of point formats that the client can parse. ', + name: 'tls.detailed.client_hello.extensions.ec_points_formats', + type: 'keyword', + }, + 'tls.detailed.client_hello.extensions._unparsed_': { + category: 'tls', + description: 'List of extensions that were left unparsed by Packetbeat. ', + name: 'tls.detailed.client_hello.extensions._unparsed_', + type: 'keyword', + }, + 'tls.detailed.server_hello.version': { + category: 'tls', + description: + 'The version of the TLS protocol that is used for this session. It is the highest version supported by the server not exceeding the version requested in the client hello. ', + name: 'tls.detailed.server_hello.version', + type: 'keyword', + }, + 'tls.detailed.server_hello.selected_compression_method': { + category: 'tls', + description: + 'The compression method selected by the server from the list provided in the client hello. ', + name: 'tls.detailed.server_hello.selected_compression_method', + type: 'keyword', + }, + 'tls.detailed.server_hello.session_id': { + category: 'tls', + description: + 'Unique number to identify the session for the corresponding connection with the client. ', + name: 'tls.detailed.server_hello.session_id', + type: 'keyword', + }, + 'tls.detailed.server_hello.extensions.application_layer_protocol_negotiation': { + category: 'tls', + description: 'Negotiated application layer protocol', + name: 'tls.detailed.server_hello.extensions.application_layer_protocol_negotiation', + type: 'keyword', + }, + 'tls.detailed.server_hello.extensions.session_ticket': { + category: 'tls', + description: + 'Used to announce that a session ticket will be provided by the server. Always an empty string. ', + name: 'tls.detailed.server_hello.extensions.session_ticket', + type: 'keyword', + }, + 'tls.detailed.server_hello.extensions.supported_versions': { + category: 'tls', + description: 'Negotiated TLS version to be used. ', + name: 'tls.detailed.server_hello.extensions.supported_versions', + type: 'keyword', + }, + 'tls.detailed.server_hello.extensions.ec_points_formats': { + category: 'tls', + description: + 'List of Elliptic Curve (EC) point formats. Indicates the set of point formats that the server can parse. ', + name: 'tls.detailed.server_hello.extensions.ec_points_formats', + type: 'keyword', + }, + 'tls.detailed.server_hello.extensions._unparsed_': { + category: 'tls', + description: 'List of extensions that were left unparsed by Packetbeat. ', + name: 'tls.detailed.server_hello.extensions._unparsed_', + type: 'keyword', + }, + 'tls.detailed.client_certificate.version': { + category: 'tls', + description: 'X509 format version.', + name: 'tls.detailed.client_certificate.version', + type: 'long', + }, + 'tls.detailed.client_certificate.version_number': { + category: 'tls', + description: 'Version of x509 format.', + example: 3, + name: 'tls.detailed.client_certificate.version_number', + type: 'keyword', + }, + 'tls.detailed.client_certificate.serial_number': { + category: 'tls', + description: "The certificate's serial number.", + name: 'tls.detailed.client_certificate.serial_number', + type: 'keyword', + }, + 'tls.detailed.client_certificate.not_before': { + category: 'tls', + description: 'Date before which the certificate is not valid.', + name: 'tls.detailed.client_certificate.not_before', + type: 'date', + }, + 'tls.detailed.client_certificate.not_after': { + category: 'tls', + description: 'Date after which the certificate expires.', + name: 'tls.detailed.client_certificate.not_after', + type: 'date', + }, + 'tls.detailed.client_certificate.public_key_algorithm': { + category: 'tls', + description: "The algorithm used for this certificate's public key. One of RSA, DSA or ECDSA. ", + name: 'tls.detailed.client_certificate.public_key_algorithm', + type: 'keyword', + }, + 'tls.detailed.client_certificate.public_key_size': { + category: 'tls', + description: 'Size of the public key.', + name: 'tls.detailed.client_certificate.public_key_size', + type: 'long', + }, + 'tls.detailed.client_certificate.signature_algorithm': { + category: 'tls', + description: "The algorithm used for the certificate's signature. ", + name: 'tls.detailed.client_certificate.signature_algorithm', + type: 'keyword', + }, + 'tls.detailed.client_certificate.alternative_names': { + category: 'tls', + description: 'Subject Alternative Names for this certificate.', + name: 'tls.detailed.client_certificate.alternative_names', + type: 'keyword', + }, + 'tls.detailed.client_certificate.subject.country': { + category: 'tls', + description: 'Country code.', + name: 'tls.detailed.client_certificate.subject.country', + type: 'keyword', + }, + 'tls.detailed.client_certificate.subject.organization': { + category: 'tls', + description: 'Organization name.', + name: 'tls.detailed.client_certificate.subject.organization', + type: 'keyword', + }, + 'tls.detailed.client_certificate.subject.organizational_unit': { + category: 'tls', + description: 'Unit within organization.', + name: 'tls.detailed.client_certificate.subject.organizational_unit', + type: 'keyword', + }, + 'tls.detailed.client_certificate.subject.province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.detailed.client_certificate.subject.province', + type: 'keyword', + }, + 'tls.detailed.client_certificate.subject.common_name': { + category: 'tls', + description: 'Name or host name identified by the certificate.', + name: 'tls.detailed.client_certificate.subject.common_name', + type: 'keyword', + }, + 'tls.detailed.client_certificate.subject.locality': { + category: 'tls', + description: 'Locality.', + name: 'tls.detailed.client_certificate.subject.locality', + type: 'keyword', + }, + 'tls.detailed.client_certificate.subject.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of the certificate subject entity.', + example: 'C=US, ST=California, L=San Francisco, O=Fastly, Inc., CN=r2.shared.global.fastly.net', + name: 'tls.detailed.client_certificate.subject.distinguished_name', + type: 'keyword', + }, + 'tls.detailed.client_certificate.issuer.country': { + category: 'tls', + description: 'Country code.', + name: 'tls.detailed.client_certificate.issuer.country', + type: 'keyword', + }, + 'tls.detailed.client_certificate.issuer.organization': { + category: 'tls', + description: 'Organization name.', + name: 'tls.detailed.client_certificate.issuer.organization', + type: 'keyword', + }, + 'tls.detailed.client_certificate.issuer.organizational_unit': { + category: 'tls', + description: 'Unit within organization.', + name: 'tls.detailed.client_certificate.issuer.organizational_unit', + type: 'keyword', + }, + 'tls.detailed.client_certificate.issuer.province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.detailed.client_certificate.issuer.province', + type: 'keyword', + }, + 'tls.detailed.client_certificate.issuer.common_name': { + category: 'tls', + description: 'Name or host name identified by the certificate.', + name: 'tls.detailed.client_certificate.issuer.common_name', + type: 'keyword', + }, + 'tls.detailed.client_certificate.issuer.locality': { + category: 'tls', + description: 'Locality.', + name: 'tls.detailed.client_certificate.issuer.locality', + type: 'keyword', + }, + 'tls.detailed.client_certificate.issuer.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of the certificate issuer entity.', + example: 'C=US, ST=California, L=San Francisco, O=Fastly, Inc., CN=r2.shared.global.fastly.net', + name: 'tls.detailed.client_certificate.issuer.distinguished_name', + type: 'keyword', + }, + 'tls.detailed.server_certificate.version': { + category: 'tls', + description: 'X509 format version.', + name: 'tls.detailed.server_certificate.version', + type: 'long', + }, + 'tls.detailed.server_certificate.version_number': { + category: 'tls', + description: 'Version of x509 format.', + example: 3, + name: 'tls.detailed.server_certificate.version_number', + type: 'keyword', + }, + 'tls.detailed.server_certificate.serial_number': { + category: 'tls', + description: "The certificate's serial number.", + name: 'tls.detailed.server_certificate.serial_number', + type: 'keyword', + }, + 'tls.detailed.server_certificate.not_before': { + category: 'tls', + description: 'Date before which the certificate is not valid.', + name: 'tls.detailed.server_certificate.not_before', + type: 'date', + }, + 'tls.detailed.server_certificate.not_after': { + category: 'tls', + description: 'Date after which the certificate expires.', + name: 'tls.detailed.server_certificate.not_after', + type: 'date', + }, + 'tls.detailed.server_certificate.public_key_algorithm': { + category: 'tls', + description: "The algorithm used for this certificate's public key. One of RSA, DSA or ECDSA. ", + name: 'tls.detailed.server_certificate.public_key_algorithm', + type: 'keyword', + }, + 'tls.detailed.server_certificate.public_key_size': { + category: 'tls', + description: 'Size of the public key.', + name: 'tls.detailed.server_certificate.public_key_size', + type: 'long', + }, + 'tls.detailed.server_certificate.signature_algorithm': { + category: 'tls', + description: "The algorithm used for the certificate's signature. ", + name: 'tls.detailed.server_certificate.signature_algorithm', + type: 'keyword', + }, + 'tls.detailed.server_certificate.alternative_names': { + category: 'tls', + description: 'Subject Alternative Names for this certificate.', + name: 'tls.detailed.server_certificate.alternative_names', + type: 'keyword', + }, + 'tls.detailed.server_certificate.subject.country': { + category: 'tls', + description: 'Country code.', + name: 'tls.detailed.server_certificate.subject.country', + type: 'keyword', + }, + 'tls.detailed.server_certificate.subject.organization': { + category: 'tls', + description: 'Organization name.', + name: 'tls.detailed.server_certificate.subject.organization', + type: 'keyword', + }, + 'tls.detailed.server_certificate.subject.organizational_unit': { + category: 'tls', + description: 'Unit within organization.', + name: 'tls.detailed.server_certificate.subject.organizational_unit', + type: 'keyword', + }, + 'tls.detailed.server_certificate.subject.province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.detailed.server_certificate.subject.province', + type: 'keyword', + }, + 'tls.detailed.server_certificate.subject.state_or_province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.detailed.server_certificate.subject.state_or_province', + type: 'keyword', + }, + 'tls.detailed.server_certificate.subject.common_name': { + category: 'tls', + description: 'Name or host name identified by the certificate.', + name: 'tls.detailed.server_certificate.subject.common_name', + type: 'keyword', + }, + 'tls.detailed.server_certificate.subject.locality': { + category: 'tls', + description: 'Locality.', + name: 'tls.detailed.server_certificate.subject.locality', + type: 'keyword', + }, + 'tls.detailed.server_certificate.subject.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of the certificate subject entity.', + example: 'C=US, ST=California, L=San Francisco, O=Fastly, Inc., CN=r2.shared.global.fastly.net', + name: 'tls.detailed.server_certificate.subject.distinguished_name', + type: 'keyword', + }, + 'tls.detailed.server_certificate.issuer.country': { + category: 'tls', + description: 'Country code.', + name: 'tls.detailed.server_certificate.issuer.country', + type: 'keyword', + }, + 'tls.detailed.server_certificate.issuer.organization': { + category: 'tls', + description: 'Organization name.', + name: 'tls.detailed.server_certificate.issuer.organization', + type: 'keyword', + }, + 'tls.detailed.server_certificate.issuer.organizational_unit': { + category: 'tls', + description: 'Unit within organization.', + name: 'tls.detailed.server_certificate.issuer.organizational_unit', + type: 'keyword', + }, + 'tls.detailed.server_certificate.issuer.province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.detailed.server_certificate.issuer.province', + type: 'keyword', + }, + 'tls.detailed.server_certificate.issuer.state_or_province': { + category: 'tls', + description: 'Province or region within country.', + name: 'tls.detailed.server_certificate.issuer.state_or_province', + type: 'keyword', + }, + 'tls.detailed.server_certificate.issuer.common_name': { + category: 'tls', + description: 'Name or host name identified by the certificate.', + name: 'tls.detailed.server_certificate.issuer.common_name', + type: 'keyword', + }, + 'tls.detailed.server_certificate.issuer.locality': { + category: 'tls', + description: 'Locality.', + name: 'tls.detailed.server_certificate.issuer.locality', + type: 'keyword', + }, + 'tls.detailed.server_certificate.issuer.distinguished_name': { + category: 'tls', + description: 'Distinguished name (DN) of the certificate issuer entity.', + example: 'C=US, ST=California, L=San Francisco, O=Fastly, Inc., CN=r2.shared.global.fastly.net', + name: 'tls.detailed.server_certificate.issuer.distinguished_name', + type: 'keyword', + }, + 'tls.detailed.server_certificate_chain': { + category: 'tls', + description: 'Chain of trust for the server certificate.', + name: 'tls.detailed.server_certificate_chain', + type: 'array', + }, + 'tls.detailed.client_certificate_chain': { + category: 'tls', + description: 'Chain of trust for the client certificate.', + name: 'tls.detailed.client_certificate_chain', + type: 'array', + }, + 'tls.detailed.alert_types': { + category: 'tls', + description: 'An array containing the TLS alert type for every alert received. ', + name: 'tls.detailed.alert_types', + type: 'keyword', + }, + 'tls.handshake_completed': { + category: 'tls', + name: 'tls.handshake_completed', + type: 'alias', + }, + 'tls.client_hello.supported_ciphers': { + category: 'tls', + name: 'tls.client_hello.supported_ciphers', + type: 'alias', + }, + 'tls.server_hello.selected_cipher': { + category: 'tls', + name: 'tls.server_hello.selected_cipher', + type: 'alias', + }, + 'tls.fingerprints.ja3': { + category: 'tls', + name: 'tls.fingerprints.ja3', + type: 'alias', + }, + 'tls.resumption_method': { + category: 'tls', + name: 'tls.resumption_method', + type: 'alias', + }, + 'tls.client_certificate_requested': { + category: 'tls', + name: 'tls.client_certificate_requested', + type: 'alias', + }, + 'tls.client_hello.version': { + category: 'tls', + name: 'tls.client_hello.version', + type: 'alias', + }, + 'tls.client_hello.session_id': { + category: 'tls', + name: 'tls.client_hello.session_id', + type: 'alias', + }, + 'tls.client_hello.supported_compression_methods': { + category: 'tls', + name: 'tls.client_hello.supported_compression_methods', + type: 'alias', + }, + 'tls.client_hello.extensions.server_name_indication': { + category: 'tls', + name: 'tls.client_hello.extensions.server_name_indication', + type: 'alias', + }, + 'tls.client_hello.extensions.application_layer_protocol_negotiation': { + category: 'tls', + name: 'tls.client_hello.extensions.application_layer_protocol_negotiation', + type: 'alias', + }, + 'tls.client_hello.extensions.session_ticket': { + category: 'tls', + name: 'tls.client_hello.extensions.session_ticket', + type: 'alias', + }, + 'tls.client_hello.extensions.supported_versions': { + category: 'tls', + name: 'tls.client_hello.extensions.supported_versions', + type: 'alias', + }, + 'tls.client_hello.extensions.supported_groups': { + category: 'tls', + name: 'tls.client_hello.extensions.supported_groups', + type: 'alias', + }, + 'tls.client_hello.extensions.signature_algorithms': { + category: 'tls', + name: 'tls.client_hello.extensions.signature_algorithms', + type: 'alias', + }, + 'tls.client_hello.extensions.ec_points_formats': { + category: 'tls', + name: 'tls.client_hello.extensions.ec_points_formats', + type: 'alias', + }, + 'tls.client_hello.extensions._unparsed_': { + category: 'tls', + name: 'tls.client_hello.extensions._unparsed_', + type: 'alias', + }, + 'tls.server_hello.version': { + category: 'tls', + name: 'tls.server_hello.version', + type: 'alias', + }, + 'tls.server_hello.selected_compression_method': { + category: 'tls', + name: 'tls.server_hello.selected_compression_method', + type: 'alias', + }, + 'tls.server_hello.session_id': { + category: 'tls', + name: 'tls.server_hello.session_id', + type: 'alias', + }, + 'tls.server_hello.extensions.application_layer_protocol_negotiation': { + category: 'tls', + name: 'tls.server_hello.extensions.application_layer_protocol_negotiation', + type: 'alias', + }, + 'tls.server_hello.extensions.session_ticket': { + category: 'tls', + name: 'tls.server_hello.extensions.session_ticket', + type: 'alias', + }, + 'tls.server_hello.extensions.supported_versions': { + category: 'tls', + name: 'tls.server_hello.extensions.supported_versions', + type: 'alias', + }, + 'tls.server_hello.extensions.ec_points_formats': { + category: 'tls', + name: 'tls.server_hello.extensions.ec_points_formats', + type: 'alias', + }, + 'tls.server_hello.extensions._unparsed_': { + category: 'tls', + name: 'tls.server_hello.extensions._unparsed_', + type: 'alias', + }, + 'tls.client_certificate.version': { + category: 'tls', + name: 'tls.client_certificate.version', + type: 'alias', + }, + 'tls.client_certificate.serial_number': { + category: 'tls', + name: 'tls.client_certificate.serial_number', + type: 'alias', + }, + 'tls.client_certificate.not_before': { + category: 'tls', + name: 'tls.client_certificate.not_before', + type: 'alias', + }, + 'tls.client_certificate.not_after': { + category: 'tls', + name: 'tls.client_certificate.not_after', + type: 'alias', + }, + 'tls.client_certificate.public_key_algorithm': { + category: 'tls', + name: 'tls.client_certificate.public_key_algorithm', + type: 'alias', + }, + 'tls.client_certificate.public_key_size': { + category: 'tls', + name: 'tls.client_certificate.public_key_size', + type: 'alias', + }, + 'tls.client_certificate.signature_algorithm': { + category: 'tls', + name: 'tls.client_certificate.signature_algorithm', + type: 'alias', + }, + 'tls.client_certificate.alternative_names': { + category: 'tls', + name: 'tls.client_certificate.alternative_names', + type: 'alias', + }, + 'tls.client_certificate.subject.country': { + category: 'tls', + name: 'tls.client_certificate.subject.country', + type: 'alias', + }, + 'tls.client_certificate.subject.organization': { + category: 'tls', + name: 'tls.client_certificate.subject.organization', + type: 'alias', + }, + 'tls.client_certificate.subject.organizational_unit': { + category: 'tls', + name: 'tls.client_certificate.subject.organizational_unit', + type: 'alias', + }, + 'tls.client_certificate.subject.province': { + category: 'tls', + name: 'tls.client_certificate.subject.province', + type: 'alias', + }, + 'tls.client_certificate.subject.common_name': { + category: 'tls', + name: 'tls.client_certificate.subject.common_name', + type: 'alias', + }, + 'tls.client_certificate.subject.locality': { + category: 'tls', + name: 'tls.client_certificate.subject.locality', + type: 'alias', + }, + 'tls.client_certificate.issuer.country': { + category: 'tls', + name: 'tls.client_certificate.issuer.country', + type: 'alias', + }, + 'tls.client_certificate.issuer.organization': { + category: 'tls', + name: 'tls.client_certificate.issuer.organization', + type: 'alias', + }, + 'tls.client_certificate.issuer.organizational_unit': { + category: 'tls', + name: 'tls.client_certificate.issuer.organizational_unit', + type: 'alias', + }, + 'tls.client_certificate.issuer.province': { + category: 'tls', + name: 'tls.client_certificate.issuer.province', + type: 'alias', + }, + 'tls.client_certificate.issuer.common_name': { + category: 'tls', + name: 'tls.client_certificate.issuer.common_name', + type: 'alias', + }, + 'tls.client_certificate.issuer.locality': { + category: 'tls', + name: 'tls.client_certificate.issuer.locality', + type: 'alias', + }, + 'tls.server_certificate.version': { + category: 'tls', + name: 'tls.server_certificate.version', + type: 'alias', + }, + 'tls.server_certificate.serial_number': { + category: 'tls', + name: 'tls.server_certificate.serial_number', + type: 'alias', + }, + 'tls.server_certificate.not_before': { + category: 'tls', + name: 'tls.server_certificate.not_before', + type: 'alias', + }, + 'tls.server_certificate.not_after': { + category: 'tls', + name: 'tls.server_certificate.not_after', + type: 'alias', + }, + 'tls.server_certificate.public_key_algorithm': { + category: 'tls', + name: 'tls.server_certificate.public_key_algorithm', + type: 'alias', + }, + 'tls.server_certificate.public_key_size': { + category: 'tls', + name: 'tls.server_certificate.public_key_size', + type: 'alias', + }, + 'tls.server_certificate.signature_algorithm': { + category: 'tls', + name: 'tls.server_certificate.signature_algorithm', + type: 'alias', + }, + 'tls.server_certificate.alternative_names': { + category: 'tls', + name: 'tls.server_certificate.alternative_names', + type: 'alias', + }, + 'tls.server_certificate.subject.country': { + category: 'tls', + name: 'tls.server_certificate.subject.country', + type: 'alias', + }, + 'tls.server_certificate.subject.organization': { + category: 'tls', + name: 'tls.server_certificate.subject.organization', + type: 'alias', + }, + 'tls.server_certificate.subject.organizational_unit': { + category: 'tls', + name: 'tls.server_certificate.subject.organizational_unit', + type: 'alias', + }, + 'tls.server_certificate.subject.province': { + category: 'tls', + name: 'tls.server_certificate.subject.province', + type: 'alias', + }, + 'tls.server_certificate.subject.common_name': { + category: 'tls', + name: 'tls.server_certificate.subject.common_name', + type: 'alias', + }, + 'tls.server_certificate.subject.locality': { + category: 'tls', + name: 'tls.server_certificate.subject.locality', + type: 'alias', + }, + 'tls.server_certificate.issuer.country': { + category: 'tls', + name: 'tls.server_certificate.issuer.country', + type: 'alias', + }, + 'tls.server_certificate.issuer.organization': { + category: 'tls', + name: 'tls.server_certificate.issuer.organization', + type: 'alias', + }, + 'tls.server_certificate.issuer.organizational_unit': { + category: 'tls', + name: 'tls.server_certificate.issuer.organizational_unit', + type: 'alias', + }, + 'tls.server_certificate.issuer.province': { + category: 'tls', + name: 'tls.server_certificate.issuer.province', + type: 'alias', + }, + 'tls.server_certificate.issuer.common_name': { + category: 'tls', + name: 'tls.server_certificate.issuer.common_name', + type: 'alias', + }, + 'tls.server_certificate.issuer.locality': { + category: 'tls', + name: 'tls.server_certificate.issuer.locality', + type: 'alias', + }, + 'tls.alert_types': { + category: 'tls', + name: 'tls.alert_types', + type: 'alias', + }, + 'winlog.api': { + category: 'winlog', + description: + 'The event log API type used to read the record. The possible values are "wineventlog" for the Windows Event Log API or "eventlogging" for the Event Logging API. The Event Logging API was designed for Windows Server 2003 or Windows 2000 operating systems. In Windows Vista, the event logging infrastructure was redesigned. On Windows Vista or later operating systems, the Windows Event Log API is used. Winlogbeat automatically detects which API to use for reading event logs. ', + name: 'winlog.api', + }, + 'winlog.activity_id': { + category: 'winlog', + description: + 'A globally unique identifier that identifies the current activity. The events that are published with this identifier are part of the same activity. ', + name: 'winlog.activity_id', + type: 'keyword', + }, + 'winlog.computer_name': { + category: 'winlog', + description: + 'The name of the computer that generated the record. When using Windows event forwarding, this name can differ from `agent.hostname`. ', + name: 'winlog.computer_name', + type: 'keyword', + }, + 'winlog.event_data': { + category: 'winlog', + description: + 'The event-specific data. This field is mutually exclusive with `user_data`. If you are capturing event data on versions prior to Windows Vista, the parameters in `event_data` are named `param1`, `param2`, and so on, because event log parameters are unnamed in earlier versions of Windows. ', + name: 'winlog.event_data', + type: 'object', + }, + 'winlog.event_data.AuthenticationPackageName': { + category: 'winlog', + name: 'winlog.event_data.AuthenticationPackageName', + type: 'keyword', + }, + 'winlog.event_data.Binary': { + category: 'winlog', + name: 'winlog.event_data.Binary', + type: 'keyword', + }, + 'winlog.event_data.BitlockerUserInputTime': { + category: 'winlog', + name: 'winlog.event_data.BitlockerUserInputTime', + type: 'keyword', + }, + 'winlog.event_data.BootMode': { + category: 'winlog', + name: 'winlog.event_data.BootMode', + type: 'keyword', + }, + 'winlog.event_data.BootType': { + category: 'winlog', + name: 'winlog.event_data.BootType', + type: 'keyword', + }, + 'winlog.event_data.BuildVersion': { + category: 'winlog', + name: 'winlog.event_data.BuildVersion', + type: 'keyword', + }, + 'winlog.event_data.Company': { + category: 'winlog', + name: 'winlog.event_data.Company', + type: 'keyword', + }, + 'winlog.event_data.CorruptionActionState': { + category: 'winlog', + name: 'winlog.event_data.CorruptionActionState', + type: 'keyword', + }, + 'winlog.event_data.CreationUtcTime': { + category: 'winlog', + name: 'winlog.event_data.CreationUtcTime', + type: 'keyword', + }, + 'winlog.event_data.Description': { + category: 'winlog', + name: 'winlog.event_data.Description', + type: 'keyword', + }, + 'winlog.event_data.Detail': { + category: 'winlog', + name: 'winlog.event_data.Detail', + type: 'keyword', + }, + 'winlog.event_data.DeviceName': { + category: 'winlog', + name: 'winlog.event_data.DeviceName', + type: 'keyword', + }, + 'winlog.event_data.DeviceNameLength': { + category: 'winlog', + name: 'winlog.event_data.DeviceNameLength', + type: 'keyword', + }, + 'winlog.event_data.DeviceTime': { + category: 'winlog', + name: 'winlog.event_data.DeviceTime', + type: 'keyword', + }, + 'winlog.event_data.DeviceVersionMajor': { + category: 'winlog', + name: 'winlog.event_data.DeviceVersionMajor', + type: 'keyword', + }, + 'winlog.event_data.DeviceVersionMinor': { + category: 'winlog', + name: 'winlog.event_data.DeviceVersionMinor', + type: 'keyword', + }, + 'winlog.event_data.DriveName': { + category: 'winlog', + name: 'winlog.event_data.DriveName', + type: 'keyword', + }, + 'winlog.event_data.DriverName': { + category: 'winlog', + name: 'winlog.event_data.DriverName', + type: 'keyword', + }, + 'winlog.event_data.DriverNameLength': { + category: 'winlog', + name: 'winlog.event_data.DriverNameLength', + type: 'keyword', + }, + 'winlog.event_data.DwordVal': { + category: 'winlog', + name: 'winlog.event_data.DwordVal', + type: 'keyword', + }, + 'winlog.event_data.EntryCount': { + category: 'winlog', + name: 'winlog.event_data.EntryCount', + type: 'keyword', + }, + 'winlog.event_data.ExtraInfo': { + category: 'winlog', + name: 'winlog.event_data.ExtraInfo', + type: 'keyword', + }, + 'winlog.event_data.FailureName': { + category: 'winlog', + name: 'winlog.event_data.FailureName', + type: 'keyword', + }, + 'winlog.event_data.FailureNameLength': { + category: 'winlog', + name: 'winlog.event_data.FailureNameLength', + type: 'keyword', + }, + 'winlog.event_data.FileVersion': { + category: 'winlog', + name: 'winlog.event_data.FileVersion', + type: 'keyword', + }, + 'winlog.event_data.FinalStatus': { + category: 'winlog', + name: 'winlog.event_data.FinalStatus', + type: 'keyword', + }, + 'winlog.event_data.Group': { + category: 'winlog', + name: 'winlog.event_data.Group', + type: 'keyword', + }, + 'winlog.event_data.IdleImplementation': { + category: 'winlog', + name: 'winlog.event_data.IdleImplementation', + type: 'keyword', + }, + 'winlog.event_data.IdleStateCount': { + category: 'winlog', + name: 'winlog.event_data.IdleStateCount', + type: 'keyword', + }, + 'winlog.event_data.ImpersonationLevel': { + category: 'winlog', + name: 'winlog.event_data.ImpersonationLevel', + type: 'keyword', + }, + 'winlog.event_data.IntegrityLevel': { + category: 'winlog', + name: 'winlog.event_data.IntegrityLevel', + type: 'keyword', + }, + 'winlog.event_data.IpAddress': { + category: 'winlog', + name: 'winlog.event_data.IpAddress', + type: 'keyword', + }, + 'winlog.event_data.IpPort': { + category: 'winlog', + name: 'winlog.event_data.IpPort', + type: 'keyword', + }, + 'winlog.event_data.KeyLength': { + category: 'winlog', + name: 'winlog.event_data.KeyLength', + type: 'keyword', + }, + 'winlog.event_data.LastBootGood': { + category: 'winlog', + name: 'winlog.event_data.LastBootGood', + type: 'keyword', + }, + 'winlog.event_data.LastShutdownGood': { + category: 'winlog', + name: 'winlog.event_data.LastShutdownGood', + type: 'keyword', + }, + 'winlog.event_data.LmPackageName': { + category: 'winlog', + name: 'winlog.event_data.LmPackageName', + type: 'keyword', + }, + 'winlog.event_data.LogonGuid': { + category: 'winlog', + name: 'winlog.event_data.LogonGuid', + type: 'keyword', + }, + 'winlog.event_data.LogonId': { + category: 'winlog', + name: 'winlog.event_data.LogonId', + type: 'keyword', + }, + 'winlog.event_data.LogonProcessName': { + category: 'winlog', + name: 'winlog.event_data.LogonProcessName', + type: 'keyword', + }, + 'winlog.event_data.LogonType': { + category: 'winlog', + name: 'winlog.event_data.LogonType', + type: 'keyword', + }, + 'winlog.event_data.MajorVersion': { + category: 'winlog', + name: 'winlog.event_data.MajorVersion', + type: 'keyword', + }, + 'winlog.event_data.MaximumPerformancePercent': { + category: 'winlog', + name: 'winlog.event_data.MaximumPerformancePercent', + type: 'keyword', + }, + 'winlog.event_data.MemberName': { + category: 'winlog', + name: 'winlog.event_data.MemberName', + type: 'keyword', + }, + 'winlog.event_data.MemberSid': { + category: 'winlog', + name: 'winlog.event_data.MemberSid', + type: 'keyword', + }, + 'winlog.event_data.MinimumPerformancePercent': { + category: 'winlog', + name: 'winlog.event_data.MinimumPerformancePercent', + type: 'keyword', + }, + 'winlog.event_data.MinimumThrottlePercent': { + category: 'winlog', + name: 'winlog.event_data.MinimumThrottlePercent', + type: 'keyword', + }, + 'winlog.event_data.MinorVersion': { + category: 'winlog', + name: 'winlog.event_data.MinorVersion', + type: 'keyword', + }, + 'winlog.event_data.NewProcessId': { + category: 'winlog', + name: 'winlog.event_data.NewProcessId', + type: 'keyword', + }, + 'winlog.event_data.NewProcessName': { + category: 'winlog', + name: 'winlog.event_data.NewProcessName', + type: 'keyword', + }, + 'winlog.event_data.NewSchemeGuid': { + category: 'winlog', + name: 'winlog.event_data.NewSchemeGuid', + type: 'keyword', + }, + 'winlog.event_data.NewTime': { + category: 'winlog', + name: 'winlog.event_data.NewTime', + type: 'keyword', + }, + 'winlog.event_data.NominalFrequency': { + category: 'winlog', + name: 'winlog.event_data.NominalFrequency', + type: 'keyword', + }, + 'winlog.event_data.Number': { + category: 'winlog', + name: 'winlog.event_data.Number', + type: 'keyword', + }, + 'winlog.event_data.OldSchemeGuid': { + category: 'winlog', + name: 'winlog.event_data.OldSchemeGuid', + type: 'keyword', + }, + 'winlog.event_data.OldTime': { + category: 'winlog', + name: 'winlog.event_data.OldTime', + type: 'keyword', + }, + 'winlog.event_data.OriginalFileName': { + category: 'winlog', + name: 'winlog.event_data.OriginalFileName', + type: 'keyword', + }, + 'winlog.event_data.Path': { + category: 'winlog', + name: 'winlog.event_data.Path', + type: 'keyword', + }, + 'winlog.event_data.PerformanceImplementation': { + category: 'winlog', + name: 'winlog.event_data.PerformanceImplementation', + type: 'keyword', + }, + 'winlog.event_data.PreviousCreationUtcTime': { + category: 'winlog', + name: 'winlog.event_data.PreviousCreationUtcTime', + type: 'keyword', + }, + 'winlog.event_data.PreviousTime': { + category: 'winlog', + name: 'winlog.event_data.PreviousTime', + type: 'keyword', + }, + 'winlog.event_data.PrivilegeList': { + category: 'winlog', + name: 'winlog.event_data.PrivilegeList', + type: 'keyword', + }, + 'winlog.event_data.ProcessId': { + category: 'winlog', + name: 'winlog.event_data.ProcessId', + type: 'keyword', + }, + 'winlog.event_data.ProcessName': { + category: 'winlog', + name: 'winlog.event_data.ProcessName', + type: 'keyword', + }, + 'winlog.event_data.ProcessPath': { + category: 'winlog', + name: 'winlog.event_data.ProcessPath', + type: 'keyword', + }, + 'winlog.event_data.ProcessPid': { + category: 'winlog', + name: 'winlog.event_data.ProcessPid', + type: 'keyword', + }, + 'winlog.event_data.Product': { + category: 'winlog', + name: 'winlog.event_data.Product', + type: 'keyword', + }, + 'winlog.event_data.PuaCount': { + category: 'winlog', + name: 'winlog.event_data.PuaCount', + type: 'keyword', + }, + 'winlog.event_data.PuaPolicyId': { + category: 'winlog', + name: 'winlog.event_data.PuaPolicyId', + type: 'keyword', + }, + 'winlog.event_data.QfeVersion': { + category: 'winlog', + name: 'winlog.event_data.QfeVersion', + type: 'keyword', + }, + 'winlog.event_data.Reason': { + category: 'winlog', + name: 'winlog.event_data.Reason', + type: 'keyword', + }, + 'winlog.event_data.SchemaVersion': { + category: 'winlog', + name: 'winlog.event_data.SchemaVersion', + type: 'keyword', + }, + 'winlog.event_data.ScriptBlockText': { + category: 'winlog', + name: 'winlog.event_data.ScriptBlockText', + type: 'keyword', + }, + 'winlog.event_data.ServiceName': { + category: 'winlog', + name: 'winlog.event_data.ServiceName', + type: 'keyword', + }, + 'winlog.event_data.ServiceVersion': { + category: 'winlog', + name: 'winlog.event_data.ServiceVersion', + type: 'keyword', + }, + 'winlog.event_data.ShutdownActionType': { + category: 'winlog', + name: 'winlog.event_data.ShutdownActionType', + type: 'keyword', + }, + 'winlog.event_data.ShutdownEventCode': { + category: 'winlog', + name: 'winlog.event_data.ShutdownEventCode', + type: 'keyword', + }, + 'winlog.event_data.ShutdownReason': { + category: 'winlog', + name: 'winlog.event_data.ShutdownReason', + type: 'keyword', + }, + 'winlog.event_data.Signature': { + category: 'winlog', + name: 'winlog.event_data.Signature', + type: 'keyword', + }, + 'winlog.event_data.SignatureStatus': { + category: 'winlog', + name: 'winlog.event_data.SignatureStatus', + type: 'keyword', + }, + 'winlog.event_data.Signed': { + category: 'winlog', + name: 'winlog.event_data.Signed', + type: 'keyword', + }, + 'winlog.event_data.StartTime': { + category: 'winlog', + name: 'winlog.event_data.StartTime', + type: 'keyword', + }, + 'winlog.event_data.State': { + category: 'winlog', + name: 'winlog.event_data.State', + type: 'keyword', + }, + 'winlog.event_data.Status': { + category: 'winlog', + name: 'winlog.event_data.Status', + type: 'keyword', + }, + 'winlog.event_data.StopTime': { + category: 'winlog', + name: 'winlog.event_data.StopTime', + type: 'keyword', + }, + 'winlog.event_data.SubjectDomainName': { + category: 'winlog', + name: 'winlog.event_data.SubjectDomainName', + type: 'keyword', + }, + 'winlog.event_data.SubjectLogonId': { + category: 'winlog', + name: 'winlog.event_data.SubjectLogonId', + type: 'keyword', + }, + 'winlog.event_data.SubjectUserName': { + category: 'winlog', + name: 'winlog.event_data.SubjectUserName', + type: 'keyword', + }, + 'winlog.event_data.SubjectUserSid': { + category: 'winlog', + name: 'winlog.event_data.SubjectUserSid', + type: 'keyword', + }, + 'winlog.event_data.TSId': { + category: 'winlog', + name: 'winlog.event_data.TSId', + type: 'keyword', + }, + 'winlog.event_data.TargetDomainName': { + category: 'winlog', + name: 'winlog.event_data.TargetDomainName', + type: 'keyword', + }, + 'winlog.event_data.TargetInfo': { + category: 'winlog', + name: 'winlog.event_data.TargetInfo', + type: 'keyword', + }, + 'winlog.event_data.TargetLogonGuid': { + category: 'winlog', + name: 'winlog.event_data.TargetLogonGuid', + type: 'keyword', + }, + 'winlog.event_data.TargetLogonId': { + category: 'winlog', + name: 'winlog.event_data.TargetLogonId', + type: 'keyword', + }, + 'winlog.event_data.TargetServerName': { + category: 'winlog', + name: 'winlog.event_data.TargetServerName', + type: 'keyword', + }, + 'winlog.event_data.TargetUserName': { + category: 'winlog', + name: 'winlog.event_data.TargetUserName', + type: 'keyword', + }, + 'winlog.event_data.TargetUserSid': { + category: 'winlog', + name: 'winlog.event_data.TargetUserSid', + type: 'keyword', + }, + 'winlog.event_data.TerminalSessionId': { + category: 'winlog', + name: 'winlog.event_data.TerminalSessionId', + type: 'keyword', + }, + 'winlog.event_data.TokenElevationType': { + category: 'winlog', + name: 'winlog.event_data.TokenElevationType', + type: 'keyword', + }, + 'winlog.event_data.TransmittedServices': { + category: 'winlog', + name: 'winlog.event_data.TransmittedServices', + type: 'keyword', + }, + 'winlog.event_data.UserSid': { + category: 'winlog', + name: 'winlog.event_data.UserSid', + type: 'keyword', + }, + 'winlog.event_data.Version': { + category: 'winlog', + name: 'winlog.event_data.Version', + type: 'keyword', + }, + 'winlog.event_data.Workstation': { + category: 'winlog', + name: 'winlog.event_data.Workstation', + type: 'keyword', + }, + 'winlog.event_data.param1': { + category: 'winlog', + name: 'winlog.event_data.param1', + type: 'keyword', + }, + 'winlog.event_data.param2': { + category: 'winlog', + name: 'winlog.event_data.param2', + type: 'keyword', + }, + 'winlog.event_data.param3': { + category: 'winlog', + name: 'winlog.event_data.param3', + type: 'keyword', + }, + 'winlog.event_data.param4': { + category: 'winlog', + name: 'winlog.event_data.param4', + type: 'keyword', + }, + 'winlog.event_data.param5': { + category: 'winlog', + name: 'winlog.event_data.param5', + type: 'keyword', + }, + 'winlog.event_data.param6': { + category: 'winlog', + name: 'winlog.event_data.param6', + type: 'keyword', + }, + 'winlog.event_data.param7': { + category: 'winlog', + name: 'winlog.event_data.param7', + type: 'keyword', + }, + 'winlog.event_data.param8': { + category: 'winlog', + name: 'winlog.event_data.param8', + type: 'keyword', + }, + 'winlog.event_id': { + category: 'winlog', + description: 'The event identifier. The value is specific to the source of the event. ', + name: 'winlog.event_id', + type: 'keyword', + }, + 'winlog.keywords': { + category: 'winlog', + description: 'The keywords are used to classify an event. ', + name: 'winlog.keywords', + type: 'keyword', + }, + 'winlog.channel': { + category: 'winlog', + description: + 'The name of the channel from which this record was read. This value is one of the names from the `event_logs` collection in the configuration. ', + name: 'winlog.channel', + type: 'keyword', + }, + 'winlog.record_id': { + category: 'winlog', + description: + 'The record ID of the event log record. The first record written to an event log is record number 1, and other records are numbered sequentially. If the record number reaches the maximum value (2^32^ for the Event Logging API and 2^64^ for the Windows Event Log API), the next record number will be 0. ', + name: 'winlog.record_id', + type: 'keyword', + }, + 'winlog.related_activity_id': { + category: 'winlog', + description: + 'A globally unique identifier that identifies the activity to which control was transferred to. The related events would then have this identifier as their `activity_id` identifier. ', + name: 'winlog.related_activity_id', + type: 'keyword', + }, + 'winlog.opcode': { + category: 'winlog', + description: + 'The opcode defined in the event. Task and opcode are typically used to identify the location in the application from where the event was logged. ', + name: 'winlog.opcode', + type: 'keyword', + }, + 'winlog.provider_guid': { + category: 'winlog', + description: + 'A globally unique identifier that identifies the provider that logged the event. ', + name: 'winlog.provider_guid', + type: 'keyword', + }, + 'winlog.process.pid': { + category: 'winlog', + description: 'The process_id of the Client Server Runtime Process. ', + name: 'winlog.process.pid', + type: 'long', + }, + 'winlog.provider_name': { + category: 'winlog', + description: + 'The source of the event log record (the application or service that logged the record). ', + name: 'winlog.provider_name', + type: 'keyword', + }, + 'winlog.task': { + category: 'winlog', + description: + 'The task defined in the event. Task and opcode are typically used to identify the location in the application from where the event was logged. The category used by the Event Logging API (on pre Windows Vista operating systems) is written to this field. ', + name: 'winlog.task', + type: 'keyword', + }, + 'winlog.process.thread.id': { + category: 'winlog', + name: 'winlog.process.thread.id', + type: 'long', + }, + 'winlog.user_data': { + category: 'winlog', + description: 'The event specific data. This field is mutually exclusive with `event_data`. ', + name: 'winlog.user_data', + type: 'object', + }, + 'winlog.user.identifier': { + category: 'winlog', + description: + 'The Windows security identifier (SID) of the account associated with this event. If Winlogbeat cannot resolve the SID to a name, then the `user.name`, `user.domain`, and `user.type` fields will be omitted from the event. If you discover Winlogbeat not resolving SIDs, review the log for clues as to what the problem may be. ', + example: 'S-1-5-21-3541430928-2051711210-1391384369-1001', + name: 'winlog.user.identifier', + type: 'keyword', + }, + 'winlog.user.name': { + category: 'winlog', + description: 'Name of the user associated with this event. ', + name: 'winlog.user.name', + type: 'keyword', + }, + 'winlog.user.domain': { + category: 'winlog', + description: 'The domain that the account associated with this event is a member of. ', + name: 'winlog.user.domain', + type: 'keyword', + }, + 'winlog.user.type': { + category: 'winlog', + description: 'The type of account associated with this event. ', + name: 'winlog.user.type', + type: 'keyword', + }, + 'winlog.version': { + category: 'winlog', + description: "The version number of the event's definition.", + name: 'winlog.version', + type: 'long', + }, + activity_id: { + category: 'base', + name: 'activity_id', + type: 'alias', + }, + computer_name: { + category: 'base', + name: 'computer_name', + type: 'alias', + }, + event_id: { + category: 'base', + name: 'event_id', + type: 'alias', + }, + keywords: { + category: 'base', + name: 'keywords', + type: 'alias', + }, + log_name: { + category: 'base', + name: 'log_name', + type: 'alias', + }, + message_error: { + category: 'base', + name: 'message_error', + type: 'alias', + }, + record_number: { + category: 'base', + name: 'record_number', + type: 'alias', + }, + related_activity_id: { + category: 'base', + name: 'related_activity_id', + type: 'alias', + }, + opcode: { + category: 'base', + name: 'opcode', + type: 'alias', + }, + provider_guid: { + category: 'base', + name: 'provider_guid', + type: 'alias', + }, + process_id: { + category: 'base', + name: 'process_id', + type: 'alias', + }, + source_name: { + category: 'base', + name: 'source_name', + type: 'alias', + }, + task: { + category: 'base', + name: 'task', + type: 'alias', + }, + thread_id: { + category: 'base', + name: 'thread_id', + type: 'alias', + }, + 'user.identifier': { + category: 'user', + name: 'user.identifier', + type: 'alias', + }, + 'user.type': { + category: 'user', + name: 'user.type', + type: 'alias', + }, + version: { + category: 'base', + name: 'version', + type: 'alias', + }, + xml: { + category: 'base', + name: 'xml', + type: 'alias', + }, + 'powershell.id': { + category: 'powershell', + description: 'Shell Id.', + example: 'Microsoft Powershell', + name: 'powershell.id', + type: 'keyword', + }, + 'powershell.pipeline_id': { + category: 'powershell', + description: 'Pipeline id.', + example: '1', + name: 'powershell.pipeline_id', + type: 'keyword', + }, + 'powershell.runspace_id': { + category: 'powershell', + description: 'Runspace id.', + example: '4fa9074d-45ab-4e53-9195-e91981ac2bbb', + name: 'powershell.runspace_id', + type: 'keyword', + }, + 'powershell.sequence': { + category: 'powershell', + description: 'Sequence number of the powershell execution.', + example: 1, + name: 'powershell.sequence', + type: 'long', + }, + 'powershell.total': { + category: 'powershell', + description: 'Total number of messages in the sequence.', + example: 10, + name: 'powershell.total', + type: 'long', + }, + 'powershell.command.path': { + category: 'powershell', + description: 'Path of the executed command.', + example: 'C:\\Windows\\system32\\cmd.exe', + name: 'powershell.command.path', + type: 'keyword', + }, + 'powershell.command.name': { + category: 'powershell', + description: 'Name of the executed command.', + example: 'cmd.exe', + name: 'powershell.command.name', + type: 'keyword', + }, + 'powershell.command.type': { + category: 'powershell', + description: 'Type of the executed command.', + example: 'Application', + name: 'powershell.command.type', + type: 'keyword', + }, + 'powershell.command.value': { + category: 'powershell', + description: 'The invoked command.', + example: 'Import-LocalizedData LocalizedData -filename ArchiveResources', + name: 'powershell.command.value', + type: 'text', + }, + 'powershell.command.invocation_details': { + category: 'powershell', + description: 'An array of objects containing detailed information of the executed command. ', + name: 'powershell.command.invocation_details', + type: 'array', + }, + 'powershell.command.invocation_details.type': { + category: 'powershell', + description: 'The type of detail.', + example: 'CommandInvocation', + name: 'powershell.command.invocation_details.type', + type: 'keyword', + }, + 'powershell.command.invocation_details.related_command': { + category: 'powershell', + description: 'The command to which the detail is related to.', + example: 'Add-Type', + name: 'powershell.command.invocation_details.related_command', + type: 'keyword', + }, + 'powershell.command.invocation_details.name': { + category: 'powershell', + description: 'Only used for ParameterBinding detail type. Indicates the parameter name. ', + example: 'AssemblyName', + name: 'powershell.command.invocation_details.name', + type: 'keyword', + }, + 'powershell.command.invocation_details.value': { + category: 'powershell', + description: 'The value of the detail. The meaning of it will depend on the detail type. ', + example: 'System.IO.Compression.FileSystem', + name: 'powershell.command.invocation_details.value', + type: 'text', + }, + 'powershell.connected_user.domain': { + category: 'powershell', + description: 'User domain.', + example: 'VAGRANT', + name: 'powershell.connected_user.domain', + type: 'keyword', + }, + 'powershell.connected_user.name': { + category: 'powershell', + description: 'User name.', + example: 'vagrant', + name: 'powershell.connected_user.name', + type: 'keyword', + }, + 'powershell.engine.version': { + category: 'powershell', + description: 'Version of the PowerShell engine version used to execute the command.', + example: '5.1.17763.1007', + name: 'powershell.engine.version', + type: 'keyword', + }, + 'powershell.engine.previous_state': { + category: 'powershell', + description: 'Previous state of the PowerShell engine. ', + example: 'Available', + name: 'powershell.engine.previous_state', + type: 'keyword', + }, + 'powershell.engine.new_state': { + category: 'powershell', + description: 'New state of the PowerShell engine. ', + example: 'Stopped', + name: 'powershell.engine.new_state', + type: 'keyword', + }, + 'powershell.file.script_block_id': { + category: 'powershell', + description: 'Id of the executed script block.', + example: '50d2dbda-7361-4926-a94d-d9eadfdb43fa', + name: 'powershell.file.script_block_id', + type: 'keyword', + }, + 'powershell.file.script_block_text': { + category: 'powershell', + description: 'Text of the executed script block. ', + example: '.\\a_script.ps1', + name: 'powershell.file.script_block_text', + type: 'text', + }, + 'powershell.process.executable_version': { + category: 'powershell', + description: 'Version of the engine hosting process executable.', + example: '5.1.17763.1007', + name: 'powershell.process.executable_version', + type: 'keyword', + }, + 'powershell.provider.new_state': { + category: 'powershell', + description: 'New state of the PowerShell provider. ', + example: 'Active', + name: 'powershell.provider.new_state', + type: 'keyword', + }, + 'powershell.provider.name': { + category: 'powershell', + description: 'Provider name. ', + example: 'Variable', + name: 'powershell.provider.name', + type: 'keyword', + }, + 'winlog.logon.type': { + category: 'winlog', + description: + 'Logon type name. This is the descriptive version of the `winlog.event_data.LogonType` ordinal. This is an enrichment added by the Security module. ', + example: 'RemoteInteractive', + name: 'winlog.logon.type', + type: 'keyword', + }, + 'winlog.logon.id': { + category: 'winlog', + description: + 'Logon ID that can be used to associate this logon with other events related to the same logon session. ', + name: 'winlog.logon.id', + type: 'keyword', + }, + 'winlog.logon.failure.reason': { + category: 'winlog', + description: 'The reason the logon failed. ', + name: 'winlog.logon.failure.reason', + type: 'keyword', + }, + 'winlog.logon.failure.status': { + category: 'winlog', + description: + 'The reason the logon failed. This is textual description based on the value of the hexadecimal `Status` field. ', + name: 'winlog.logon.failure.status', + type: 'keyword', + }, + 'winlog.logon.failure.sub_status': { + category: 'winlog', + description: + 'Additional information about the logon failure. This is a textual description based on the value of the hexidecimal `SubStatus` field. ', + name: 'winlog.logon.failure.sub_status', + type: 'keyword', + }, + 'sysmon.dns.status': { + category: 'sysmon', + description: 'Windows status code returned for the DNS query.', + name: 'sysmon.dns.status', + type: 'keyword', + }, + 'sysmon.file.archived': { + category: 'sysmon', + description: 'Indicates if the deleted file was archived.', + name: 'sysmon.file.archived', + type: 'boolean', + }, + 'sysmon.file.is_executable': { + category: 'sysmon', + description: 'Indicates if the deleted file was an executable.', + name: 'sysmon.file.is_executable', + type: 'boolean', + }, +}; diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts deleted file mode 100644 index 29944edf382f4..0000000000000 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/index.test.ts +++ /dev/null @@ -1,397 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { cloneDeep, isArray } from 'lodash/fp'; - -import { convertSchemaToAssociativeArray, getIndexSchemaDoc } from '.'; -import { auditbeatSchema, filebeatSchema, packetbeatSchema } from './8.0.0'; -import { Schema } from './type'; - -describe('Schema Beat', () => { - describe('Transform Schema documentation to an associative array', () => { - test('Auditbeat transformation', async () => { - const convertData: Schema = cloneDeep(auditbeatSchema).slice(0, 1); - convertData[0].fields = isArray(convertData[0].fields) - ? convertData[0].fields!.slice(0, 6) - : []; - - expect(convertSchemaToAssociativeArray(convertData)).toEqual({ - '@timestamp': { - description: - 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', - example: '2016-05-23T08:05:34.853Z', - name: '@timestamp', - type: 'date', - }, - labels: { - description: - 'Custom key/value pairs.\n\nCan be used to add meta information to events. Should not contain nested objects.\nAll values are stored as keyword.\n\nExample: `docker` and `k8s` labels.', - example: '{"application": "foo-bar", "env": "production"}', - name: 'labels', - type: 'object', - }, - message: { - description: - 'For log events the message field contains the log message, optimized\nfor viewing in a log viewer.\n\nFor structured logs without an original message field, other fields can be concatenated\nto form a human-readable summary of the event.\n\nIf multiple messages exist, they can be combined into one message.', - example: 'Hello World', - name: 'message', - type: 'text', - }, - tags: { - description: 'List of keywords used to tag each event.', - example: '["production", "env2"]', - name: 'tags', - type: 'keyword', - }, - agent: { - description: - 'The agent fields contain the data about the software entity, if\nany, that collects, detects, or observes events on a host, or takes measurements\non a host.\n\nExamples include Beats. Agents may also run on observers. ECS agent.* fields\nshall be populated with details of the agent running on the host or observer\nwhere the event happened or the measurement was taken.', - name: 'agent', - type: 'group', - fields: { - 'agent.ephemeral_id': { - description: - 'Ephemeral identifier of this agent (if one exists).\n\nThis id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - name: 'ephemeral_id', - type: 'keyword', - }, - 'agent.id': { - description: - 'Unique identifier of this agent (if one exists).\n\nExample: For Beats this would be beat.id.', - example: '8a4f500d', - name: 'id', - type: 'keyword', - }, - 'agent.name': { - description: - 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', - example: 'foo', - name: 'name', - type: 'keyword', - }, - 'agent.type': { - description: - 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', - example: 'filebeat', - name: 'type', - type: 'keyword', - }, - 'agent.version': { - description: 'Version of the agent.', - example: '6.0.0-rc2', - name: 'version', - type: 'keyword', - }, - }, - }, - as: { - description: - 'An autonomous system (AS) is a collection of connected Internet Protocol\n(IP) routing prefixes under the control of one or more network operators on\nbehalf of a single administrative entity or domain that presents a common, clearly\ndefined routing policy to the internet.', - name: 'as', - type: 'group', - fields: { - 'as.number': { - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - name: 'number', - type: 'long', - }, - 'as.organization.name': { - description: 'Organization name.', - example: 'Google LLC', - name: 'organization.name', - type: 'keyword', - }, - }, - }, - }); - }); - - test('Filebeat transformation', async () => { - const convertData: Schema = cloneDeep(filebeatSchema).slice(0, 1); - convertData[0].fields = isArray(convertData[0].fields) - ? convertData[0].fields!.slice(0, 6) - : []; - - expect(convertSchemaToAssociativeArray(convertData)).toEqual({ - '@timestamp': { - description: - 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', - example: '2016-05-23T08:05:34.853Z', - name: '@timestamp', - type: 'date', - }, - labels: { - description: - 'Custom key/value pairs.\n\nCan be used to add meta information to events. Should not contain nested objects.\nAll values are stored as keyword.\n\nExample: `docker` and `k8s` labels.', - example: '{"application": "foo-bar", "env": "production"}', - name: 'labels', - type: 'object', - }, - message: { - description: - 'For log events the message field contains the log message, optimized\nfor viewing in a log viewer.\n\nFor structured logs without an original message field, other fields can be concatenated\nto form a human-readable summary of the event.\n\nIf multiple messages exist, they can be combined into one message.', - example: 'Hello World', - name: 'message', - type: 'text', - }, - tags: { - description: 'List of keywords used to tag each event.', - example: '["production", "env2"]', - name: 'tags', - type: 'keyword', - }, - agent: { - description: - 'The agent fields contain the data about the software entity, if\nany, that collects, detects, or observes events on a host, or takes measurements\non a host.\n\nExamples include Beats. Agents may also run on observers. ECS agent.* fields\nshall be populated with details of the agent running on the host or observer\nwhere the event happened or the measurement was taken.', - name: 'agent', - type: 'group', - fields: { - 'agent.ephemeral_id': { - description: - 'Ephemeral identifier of this agent (if one exists).\n\nThis id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - name: 'ephemeral_id', - type: 'keyword', - }, - 'agent.id': { - description: - 'Unique identifier of this agent (if one exists).\n\nExample: For Beats this would be beat.id.', - example: '8a4f500d', - name: 'id', - type: 'keyword', - }, - 'agent.name': { - description: - 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', - example: 'foo', - name: 'name', - type: 'keyword', - }, - 'agent.type': { - description: - 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', - example: 'filebeat', - name: 'type', - type: 'keyword', - }, - 'agent.version': { - description: 'Version of the agent.', - example: '6.0.0-rc2', - name: 'version', - type: 'keyword', - }, - }, - }, - as: { - description: - 'An autonomous system (AS) is a collection of connected Internet Protocol\n(IP) routing prefixes under the control of one or more network operators on\nbehalf of a single administrative entity or domain that presents a common, clearly\ndefined routing policy to the internet.', - name: 'as', - type: 'group', - fields: { - 'as.number': { - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - name: 'number', - type: 'long', - }, - 'as.organization.name': { - description: 'Organization name.', - example: 'Google LLC', - name: 'organization.name', - type: 'keyword', - }, - }, - }, - }); - }); - - test('Packetbeat transformation', async () => { - const convertData: Schema = cloneDeep(packetbeatSchema).slice(0, 1); - convertData[0].fields = isArray(convertData[0].fields) - ? convertData[0].fields!.slice(0, 6) - : []; - - expect(convertSchemaToAssociativeArray(convertData)).toEqual({ - '@timestamp': { - description: - 'Date/time when the event originated.\n\nThis is the date/time extracted from the event, typically representing when\nthe event was generated by the source.\n\nIf the event source has no original timestamp, this value is typically populated\nby the first time the event was received by the pipeline.\n\nRequired field for all events.', - example: '2016-05-23T08:05:34.853Z', - name: '@timestamp', - type: 'date', - }, - labels: { - description: - 'Custom key/value pairs.\n\nCan be used to add meta information to events. Should not contain nested objects.\nAll values are stored as keyword.\n\nExample: `docker` and `k8s` labels.', - example: '{"application": "foo-bar", "env": "production"}', - name: 'labels', - type: 'object', - }, - message: { - description: - 'For log events the message field contains the log message, optimized\nfor viewing in a log viewer.\n\nFor structured logs without an original message field, other fields can be concatenated\nto form a human-readable summary of the event.\n\nIf multiple messages exist, they can be combined into one message.', - example: 'Hello World', - name: 'message', - type: 'text', - }, - tags: { - description: 'List of keywords used to tag each event.', - example: '["production", "env2"]', - name: 'tags', - type: 'keyword', - }, - agent: { - description: - 'The agent fields contain the data about the software entity, if\nany, that collects, detects, or observes events on a host, or takes measurements\non a host.\n\nExamples include Beats. Agents may also run on observers. ECS agent.* fields\nshall be populated with details of the agent running on the host or observer\nwhere the event happened or the measurement was taken.', - name: 'agent', - type: 'group', - fields: { - 'agent.ephemeral_id': { - description: - 'Ephemeral identifier of this agent (if one exists).\n\nThis id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - name: 'ephemeral_id', - type: 'keyword', - }, - 'agent.id': { - description: - 'Unique identifier of this agent (if one exists).\n\nExample: For Beats this would be beat.id.', - example: '8a4f500d', - name: 'id', - type: 'keyword', - }, - 'agent.name': { - description: - 'Custom name of the agent.\n\nThis is a name that can be given to an agent. This can be helpful if for example\ntwo Filebeat instances are running on the same host but a human readable separation\nis needed on which Filebeat instance data is coming from.\n\nIf no name is given, the name is often left empty.', - example: 'foo', - name: 'name', - type: 'keyword', - }, - 'agent.type': { - description: - 'Type of the agent.\n\nThe agent type stays always the same and should be given by the agent used.\nIn case of Filebeat the agent would always be Filebeat also if two Filebeat\ninstances are run on the same machine.', - example: 'filebeat', - name: 'type', - type: 'keyword', - }, - 'agent.version': { - description: 'Version of the agent.', - example: '6.0.0-rc2', - name: 'version', - type: 'keyword', - }, - }, - }, - as: { - description: - 'An autonomous system (AS) is a collection of connected Internet Protocol\n(IP) routing prefixes under the control of one or more network operators on\nbehalf of a single administrative entity or domain that presents a common, clearly\ndefined routing policy to the internet.', - name: 'as', - type: 'group', - fields: { - 'as.number': { - description: - 'Unique number allocated to the autonomous system. The autonomous\nsystem number (ASN) uniquely identifies each network on the Internet.', - example: 15169, - name: 'number', - type: 'long', - }, - 'as.organization.name': { - description: 'Organization name.', - example: 'Google LLC', - name: 'organization.name', - type: 'keyword', - }, - }, - }, - }); - }); - }); - - describe('GetIndexSchemaDoc', () => { - test('Filebeat transformation', async () => { - expect(Object.keys(getIndexSchemaDoc('auditbeat'))).toEqual([ - '_id', - '_index', - '@timestamp', - 'labels', - 'message', - 'tags', - 'agent', - 'as', - 'client', - 'cloud', - 'code_signature', - 'container', - 'destination', - 'dll', - 'dns', - 'ecs', - 'error', - 'event', - 'file', - 'geo', - 'group', - 'hash', - 'host', - 'http', - 'interface', - 'log', - 'network', - 'observer', - 'organization', - 'os', - 'package', - 'pe', - 'process', - 'registry', - 'related', - 'rule', - 'server', - 'service', - 'source', - 'threat', - 'tls', - 'tracing', - 'url', - 'user', - 'user_agent', - 'vlan', - 'vulnerability', - 'agent.hostname', - 'beat.timezone', - 'fields', - 'beat.name', - 'beat.hostname', - 'timeseries.instance', - 'cloud.project.id', - 'cloud.image.id', - 'meta.cloud.provider', - 'meta.cloud.instance_id', - 'meta.cloud.instance_name', - 'meta.cloud.machine_type', - 'meta.cloud.availability_zone', - 'meta.cloud.project_id', - 'meta.cloud.region', - 'docker', - 'kubernetes', - 'jolokia.agent.version', - 'jolokia.agent.id', - 'jolokia.server.product', - 'jolokia.server.version', - 'jolokia.server.vendor', - 'jolokia.url', - 'jolokia.secured', - 'auditd', - 'geoip', - 'socket', - 'system.audit', - ]); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts deleted file mode 100644 index 58627a199a181..0000000000000 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/index.ts +++ /dev/null @@ -1,129 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get, has, isArray, isEmpty, isNumber, isString, memoize, pick } from 'lodash/fp'; - -import { - auditbeatSchema, - baseCategoryFields, - ecsSchema, - extraSchemaField, - filebeatSchema, - packetbeatSchema, - winlogbeatSchema, -} from './8.0.0'; -import { OutputSchema, Schema, SchemaFields, SchemaItem } from './type'; - -export * from './type'; -export { baseCategoryFields }; -export const convertSchemaToAssociativeArray = (schema: Schema): OutputSchema => - schema.reduce((accumulator: OutputSchema, item: Partial) => { - if (item.fields != null && !isEmpty(item.fields)) { - return { - ...accumulator, - ...convertFieldsToAssociativeArray(item), - }; - } - return accumulator; - }, {}); - -const paramsToPick = ['description', 'example', 'name', 'type', 'format']; - -const onlyStringOrNumber = (fields: object) => - Object.keys(fields).reduce((acc, item) => { - const value = get(item, fields); - return { - ...acc, - [item]: isString(value) || isNumber(value) ? value : JSON.stringify(value), - }; - }, {}); - -const convertFieldsToAssociativeArray = ( - schemaFields: Partial, - path: string = '' -): OutputSchema => - schemaFields.fields && isArray(schemaFields.fields) - ? schemaFields.fields.reduce((accumulator: OutputSchema, item: Partial) => { - if (item.name) { - const attr = isEmpty(path) ? item.name : `${path}.${item.name}`; - if (!isEmpty(item.fields) && isEmpty(path)) { - return { - ...accumulator, - [attr]: { - ...onlyStringOrNumber(pick(paramsToPick, item)), - fields: { - ...convertFieldsToAssociativeArray(item, attr), - }, - }, - }; - } else if (!isEmpty(item.fields) && !isEmpty(path)) { - return { - ...accumulator, - [attr]: onlyStringOrNumber(pick(paramsToPick, item)), - ...convertFieldsToAssociativeArray(item, attr), - }; - } else { - return { - ...accumulator, - [attr]: onlyStringOrNumber(pick(paramsToPick, item)), - }; - } - } - return accumulator; - }, {}) - : {}; - -export const getIndexSchemaDoc = memoize((index: string) => { - if (index.match('auditbeat') != null) { - return { - ...extraSchemaField, - ...convertSchemaToAssociativeArray(auditbeatSchema), - }; - } else if (index.match('filebeat') != null) { - return { - ...extraSchemaField, - ...convertSchemaToAssociativeArray(filebeatSchema), - }; - } else if (index.match('packetbeat') != null) { - return { - ...extraSchemaField, - ...convertSchemaToAssociativeArray(packetbeatSchema), - }; - } else if (index.match('winlogbeat') != null) { - return { - ...extraSchemaField, - ...convertSchemaToAssociativeArray(winlogbeatSchema), - }; - } - return { - ...extraSchemaField, - ...convertSchemaToAssociativeArray(ecsSchema), - }; -}); - -export const hasDocumentation = (index: string, path: string): boolean => { - const splitPath = path.split('.'); - const category = splitPath.length > 0 ? splitPath[0] : null; - if (category === null) { - return false; - } - if (splitPath.length > 1) { - return has([category, 'fields', path], getIndexSchemaDoc(index)); - } - return has(category, getIndexSchemaDoc(index)); -}; - -export const getDocumentation = (index: string, path: string) => { - const splitPath = path.split('.'); - const category = splitPath.length > 0 ? splitPath[0] : null; - if (category === null) { - return {}; - } - if (splitPath.length > 1) { - return get([category, 'fields', path], getIndexSchemaDoc(index)) || {}; - } - return get(category, getIndexSchemaDoc(index)) || {}; -}; diff --git a/x-pack/plugins/security_solution/server/utils/beat_schema/type.ts b/x-pack/plugins/security_solution/server/utils/beat_schema/type.ts deleted file mode 100644 index 722589ce7e2bb..0000000000000 --- a/x-pack/plugins/security_solution/server/utils/beat_schema/type.ts +++ /dev/null @@ -1,73 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -/* - * BEAT Interface - * - */ - -export interface SchemaFields { - default_field: boolean; - default_fields: boolean; - definition: string; - deprecated: string; - description: string; - doc_values: boolean; - example: string | number | object | boolean; - footnote: string; - format: string; - group: number; - index: boolean; - ignore_above: number; - input_format: string; - level: string; - migration: boolean; - multi_fields: object[]; - name: string; - norms: boolean; - object_type: string; - object_type_mapping_type: string; - output_format: string; - output_precision: number; - overwrite: boolean; - path: string; - possible_values: string[] | number[]; - release: string; - required: boolean; - reusable: object; - short: string; - title: string; - type: string; - fields: Array>; -} - -export interface SchemaItem { - anchor: string; - key: string; - title: string; - description: string; - short_config: boolean; - release: string; - fields: Array>; -} - -export type Schema = Array>; - -/* - * Associative Array Output Interface - * - */ - -export interface RequiredSchemaField { - description: string; - example: string | number; - name: string; - type: string; - format: string; - fields: Readonly>>; -} - -export type OutputSchema = Readonly>>; diff --git a/x-pack/test/api_integration/apis/security_solution/sources.ts b/x-pack/test/api_integration/apis/security_solution/sources.ts index f99dd4c65fc83..1ec4bfda8492d 100644 --- a/x-pack/test/api_integration/apis/security_solution/sources.ts +++ b/x-pack/test/api_integration/apis/security_solution/sources.ts @@ -5,110 +5,107 @@ */ import expect from '@kbn/expect'; -import { sourceQuery } from '../../../../plugins/security_solution/public/common/containers/source/index.gql_query'; -import { SourceQuery } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const client = getService('securitySolutionGraphQLClient'); + const supertest = getService('supertest'); describe('sources', () => { before(() => esArchiver.load('auditbeat/default')); after(() => esArchiver.unload('auditbeat/default')); it('Make sure that we get source information when auditbeat indices is there', async () => { - const resp = await client.query({ - query: sourceQuery, - variables: { - sourceId: 'default', - defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - docValueFields: [], - }, - }); - const sourceStatus = resp.data.source.status; - // test data in x-pack/test/functional/es_archives/auditbeat_test_data/data.json.gz - expect(sourceStatus.indexFields.length).to.be(397); - expect(sourceStatus.indicesExist).to.be(true); + const { body: sourceStatus } = await supertest + .post('/internal/search/securitySolutionIndexFields/') + .set('kbn-xsrf', 'true') + .send({ + indices: ['auditbeat-*'], + onlyCheckIfIndicesExist: false, + }) + .expect(200); + + expect(sourceStatus.indexFields.length).to.be(351); + expect(sourceStatus.indicesExist).to.eql(['auditbeat-*']); }); it('should find indexes as being available when they exist', async () => { - const resp = await client.query({ - query: sourceQuery, - variables: { - sourceId: 'default', - defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - docValueFields: [], - }, - }); - const sourceStatus = resp.data.source.status; - expect(sourceStatus.indicesExist).to.be(true); + const { body: sourceStatus } = await supertest + .post('/internal/search/securitySolutionIndexFields/') + .set('kbn-xsrf', 'true') + .send({ + indices: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + onlyCheckIfIndicesExist: false, + }) + .expect(200); + + expect(sourceStatus.indicesExist).to.eql(['auditbeat-*', 'winlogbeat-*']); }); it('should not find indexes as existing when there is an empty array of them', async () => { - const resp = await client.query({ - query: sourceQuery, - variables: { - sourceId: 'default', - defaultIndex: [], - docValueFields: [], - }, - }); - const sourceStatus = resp.data.source.status; - expect(sourceStatus.indicesExist).to.be(false); + const { body: sourceStatus } = await supertest + .post('/internal/search/securitySolutionIndexFields/') + .set('kbn-xsrf', 'true') + .send({ + indices: [], + onlyCheckIfIndicesExist: false, + }) + .expect(200); + + expect(sourceStatus.indicesExist).to.eql([]); }); it('should not find indexes as existing when there is a _all within it', async () => { - const resp = await client.query({ - query: sourceQuery, - variables: { - sourceId: 'default', - defaultIndex: ['_all'], - docValueFields: [], - }, - }); - const sourceStatus = resp.data.source.status; - expect(sourceStatus.indicesExist).to.be(false); + const { body: sourceStatus } = await supertest + .post('/internal/search/securitySolutionIndexFields/') + .set('kbn-xsrf', 'true') + .send({ + indices: ['_all'], + onlyCheckIfIndicesExist: false, + }) + .expect(200); + + expect(sourceStatus.indicesExist).to.eql([]); }); it('should not find indexes as existing when there are empty strings within it', async () => { - const resp = await client.query({ - query: sourceQuery, - variables: { - sourceId: 'default', - defaultIndex: [''], - docValueFields: [], - }, - }); - const sourceStatus = resp.data.source.status; - expect(sourceStatus.indicesExist).to.be(false); + const { body: sourceStatus } = await supertest + .post('/internal/search/securitySolutionIndexFields/') + .set('kbn-xsrf', 'true') + .send({ + indices: [''], + onlyCheckIfIndicesExist: false, + }) + .expect(200); + + expect(sourceStatus.indicesExist).to.eql([]); }); it('should not find indexes as existing when there are blank spaces within it', async () => { - const resp = await client.query({ - query: sourceQuery, - variables: { - sourceId: 'default', - defaultIndex: [' '], - docValueFields: [], - }, - }); - const sourceStatus = resp.data.source.status; - expect(sourceStatus.indicesExist).to.be(false); + const { body: sourceStatus } = await supertest + .post('/internal/search/securitySolutionIndexFields/') + .set('kbn-xsrf', 'true') + .send({ + indices: [' '], + onlyCheckIfIndicesExist: false, + }) + .expect(200); + + expect(sourceStatus.indicesExist).to.eql([]); }); it('should find indexes when one is an empty index but the others are valid', async () => { - const resp = await client.query({ - query: sourceQuery, - variables: { - sourceId: 'default', - defaultIndex: ['', 'auditbeat-*'], - docValueFields: [], - }, - }); - const sourceStatus = resp.data.source.status; - expect(sourceStatus.indicesExist).to.be(true); + const { body: sourceStatus } = await supertest + .post('/internal/search/securitySolutionIndexFields/') + .set('kbn-xsrf', 'true') + .send({ + indices: ['', 'auditbeat-*'], + onlyCheckIfIndicesExist: false, + }) + .expect(200); + + expect(sourceStatus.indicesExist).to.eql(['auditbeat-*']); }); }); } From 27c32edd232caa325d345ff5a1cc8479f4a41be7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Wed, 23 Sep 2020 22:21:34 +0200 Subject: [PATCH 67/92] [Security Solution] Cleanup Overview graphql (#78272) * [Security Solution] Cleanup Tls graphql * [Security Solution] Cleanup Uncommon Processes graphql * [Security Solution] Cleanup Overview graphql --- .../security_solution/hosts/overview/index.ts | 2 +- .../security_solution/index.ts | 4 +- .../public/graphql/introspection.json | 336 --------------- .../security_solution/public/graphql/types.ts | 206 --------- .../overview_host_stats/index.test.tsx | 6 +- .../components/overview_host_stats/index.tsx | 8 +- .../components/overview_host_stats/mock.ts | 38 +- .../overview_network_stats/index.test.tsx | 8 +- .../overview_network_stats/index.tsx | 8 +- .../components/overview_network_stats/mock.ts | 24 +- .../overview_host/index.gql_query.ts | 43 -- .../containers/overview_host/index.tsx | 6 +- .../overview_network/index.gql_query.ts | 40 -- .../security_solution/server/graphql/index.ts | 2 - .../server/graphql/overview/index.ts | 8 - .../server/graphql/overview/resolvers.ts | 45 -- .../server/graphql/overview/schema.gql.ts | 57 --- .../security_solution/server/graphql/types.ts | 319 -------------- .../security_solution/server/init_server.ts | 2 - .../server/lib/compose/kibana.ts | 3 - .../lib/overview/elastic_adapter.test.ts | 187 --------- .../lib/overview/elasticsearch_adapter.ts | 132 ------ .../server/lib/overview/index.ts | 28 -- .../server/lib/overview/mock.ts | 176 -------- .../server/lib/overview/query.dsl.ts | 397 ------------------ .../server/lib/overview/types.ts | 109 ----- .../security_solution/server/lib/types.ts | 2 - .../factory/hosts/overview/index.ts | 4 +- .../apis/security_solution/index.js | 4 +- .../apis/security_solution/overview_host.ts | 2 + .../security_solution/overview_network.ts | 2 + 31 files changed, 59 insertions(+), 2149 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_host/index.gql_query.ts delete mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_network/index.gql_query.ts delete mode 100644 x-pack/plugins/security_solution/server/graphql/overview/index.ts delete mode 100644 x-pack/plugins/security_solution/server/graphql/overview/resolvers.ts delete mode 100644 x-pack/plugins/security_solution/server/graphql/overview/schema.gql.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/overview/elastic_adapter.test.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/overview/elasticsearch_adapter.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/overview/index.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/overview/mock.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/overview/query.dsl.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/overview/types.ts diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts index 569ed611bd35b..4416cbb023f10 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/overview/index.ts @@ -10,7 +10,7 @@ import { RequestBasicOptions } from '../..'; export type HostOverviewRequestOptions = RequestBasicOptions; -export interface HostOverviewStrategyResponse extends IEsSearchResponse { +export interface HostsOverviewStrategyResponse extends IEsSearchResponse { inspect?: Maybe; overviewHost: { auditbeatAuditd?: Maybe; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index af9faef89af46..39443e596273a 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -9,7 +9,7 @@ import { ESQuery } from '../../typed_json'; import { HostDetailsStrategyResponse, HostDetailsRequestOptions, - HostOverviewStrategyResponse, + HostsOverviewStrategyResponse, HostAuthenticationsRequestOptions, HostAuthenticationsStrategyResponse, HostOverviewRequestOptions, @@ -107,7 +107,7 @@ export type StrategyResponseType = T extends HostsQ : T extends HostsQueries.details ? HostDetailsStrategyResponse : T extends HostsQueries.overview - ? HostOverviewStrategyResponse + ? HostsOverviewStrategyResponse : T extends HostsQueries.authentications ? HostAuthenticationsStrategyResponse : T extends HostsQueries.firstLastSeen diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 568a960f0804e..2f312c461ff8c 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -2088,104 +2088,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "OverviewNetwork", - "description": "", - "args": [ - { - "name": "id", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "timerange", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "filterQuery", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "defaultIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "defaultValue": null - } - ], - "type": { "kind": "OBJECT", "name": "OverviewNetworkData", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "OverviewHost", - "description": "", - "args": [ - { - "name": "id", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "timerange", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "filterQuery", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "defaultIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "defaultValue": null - } - ], - "type": { "kind": "OBJECT", "name": "OverviewHostData", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "whoAmI", "description": "Just a simple example to get the app name", @@ -8901,244 +8803,6 @@ "enumValues": null, "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "OverviewNetworkData", - "description": "", - "fields": [ - { - "name": "auditbeatSocket", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "filebeatCisco", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "filebeatNetflow", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "filebeatPanw", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "filebeatSuricata", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "filebeatZeek", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "packetbeatDNS", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "packetbeatFlow", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "packetbeatTLS", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "OverviewHostData", - "description": "", - "fields": [ - { - "name": "auditbeatAuditd", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "auditbeatFIM", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "auditbeatLogin", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "auditbeatPackage", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "auditbeatProcess", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "auditbeatUser", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "endgameDns", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "endgameFile", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "endgameImageLoad", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "endgameNetwork", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "endgameProcess", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "endgameRegistry", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "endgameSecurity", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "filebeatSystemModule", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "winlogbeatSecurity", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "winlogbeatMWSysmonOperational", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "SayMyName", diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index 0bce952912c5c..bcb580a1a2988 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -556,10 +556,6 @@ export interface Source { NetworkDnsHistogram: NetworkDsOverTimeData; NetworkHttp: NetworkHttpData; - - OverviewNetwork?: Maybe; - - OverviewHost?: Maybe; /** Just a simple example to get the app name */ whoAmI?: Maybe; } @@ -1832,64 +1828,6 @@ export interface NetworkHttpItem { statuses: string[]; } -export interface OverviewNetworkData { - auditbeatSocket?: Maybe; - - filebeatCisco?: Maybe; - - filebeatNetflow?: Maybe; - - filebeatPanw?: Maybe; - - filebeatSuricata?: Maybe; - - filebeatZeek?: Maybe; - - packetbeatDNS?: Maybe; - - packetbeatFlow?: Maybe; - - packetbeatTLS?: Maybe; - - inspect?: Maybe; -} - -export interface OverviewHostData { - auditbeatAuditd?: Maybe; - - auditbeatFIM?: Maybe; - - auditbeatLogin?: Maybe; - - auditbeatPackage?: Maybe; - - auditbeatProcess?: Maybe; - - auditbeatUser?: Maybe; - - endgameDns?: Maybe; - - endgameFile?: Maybe; - - endgameImageLoad?: Maybe; - - endgameNetwork?: Maybe; - - endgameProcess?: Maybe; - - endgameRegistry?: Maybe; - - endgameSecurity?: Maybe; - - filebeatSystemModule?: Maybe; - - winlogbeatSecurity?: Maybe; - - winlogbeatMWSysmonOperational?: Maybe; - - inspect?: Maybe; -} - export interface SayMyName { /** The id of the source */ appName: string; @@ -2487,24 +2425,6 @@ export interface NetworkHttpSourceArgs { defaultIndex: string[]; } -export interface OverviewNetworkSourceArgs { - id?: Maybe; - - timerange: TimerangeInput; - - filterQuery?: Maybe; - - defaultIndex: string[]; -} -export interface OverviewHostSourceArgs { - id?: Maybe; - - timerange: TimerangeInput; - - filterQuery?: Maybe; - - defaultIndex: string[]; -} export interface IndicesExistSourceStatusArgs { defaultIndex: string[]; } @@ -3957,132 +3877,6 @@ export namespace GetUsersQuery { }; } -export namespace GetOverviewHostQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - filterQuery?: Maybe; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - OverviewHost: Maybe; - }; - - export type OverviewHost = { - __typename?: 'OverviewHostData'; - - auditbeatAuditd: Maybe; - - auditbeatFIM: Maybe; - - auditbeatLogin: Maybe; - - auditbeatPackage: Maybe; - - auditbeatProcess: Maybe; - - auditbeatUser: Maybe; - - endgameDns: Maybe; - - endgameFile: Maybe; - - endgameImageLoad: Maybe; - - endgameNetwork: Maybe; - - endgameProcess: Maybe; - - endgameRegistry: Maybe; - - endgameSecurity: Maybe; - - filebeatSystemModule: Maybe; - - winlogbeatSecurity: Maybe; - - winlogbeatMWSysmonOperational: Maybe; - - inspect: Maybe; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - -export namespace GetOverviewNetworkQuery { - export type Variables = { - sourceId: string; - timerange: TimerangeInput; - filterQuery?: Maybe; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - OverviewNetwork: Maybe; - }; - - export type OverviewNetwork = { - __typename?: 'OverviewNetworkData'; - - auditbeatSocket: Maybe; - - filebeatCisco: Maybe; - - filebeatNetflow: Maybe; - - filebeatPanw: Maybe; - - filebeatSuricata: Maybe; - - filebeatZeek: Maybe; - - packetbeatDNS: Maybe; - - packetbeatFlow: Maybe; - - packetbeatTLS: Maybe; - - inspect: Maybe; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - export namespace GetAllTimeline { export type Variables = { pageInfo: PageInfoTimeline; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host_stats/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host_stats/index.test.tsx index 75295d9e45c0c..3a12f0c038b8c 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host_stats/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host_stats/index.test.tsx @@ -14,7 +14,7 @@ import { TestProviders } from '../../../common/mock/test_providers'; describe('Overview Host Stat Data', () => { describe('rendering', () => { test('it renders the default OverviewHostStats', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); }); @@ -22,7 +22,7 @@ describe('Overview Host Stat Data', () => { test('it does NOT show loading indicator when loading is false', () => { const wrapper = mount( - + ); @@ -42,7 +42,7 @@ describe('Overview Host Stat Data', () => { test('it shows loading indicator when loading is true', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host_stats/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host_stats/index.tsx index 92250ed3c549b..ef595476d8a94 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host_stats/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host_stats/index.tsx @@ -9,16 +9,18 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import styled from 'styled-components'; -import { OverviewHostData } from '../../../graphql/types'; +import { HostsOverviewStrategyResponse } from '../../../../common/search_strategy'; import { FormattedStat, StatGroup } from '../types'; import { StatValue } from '../stat_value'; interface OverviewHostProps { - data: OverviewHostData; + data: HostsOverviewStrategyResponse['overviewHost']; loading: boolean; } -export const getOverviewHostStats = (data: OverviewHostData): FormattedStat[] => [ +export const getOverviewHostStats = ( + data: HostsOverviewStrategyResponse['overviewHost'] +): FormattedStat[] => [ { count: data.auditbeatAuditd ?? 0, title: ( diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host_stats/mock.ts b/x-pack/plugins/security_solution/public/overview/components/overview_host_stats/mock.ts index 63b3a484c1eaa..986d02faac37a 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host_stats/mock.ts +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host_stats/mock.ts @@ -4,25 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { OverviewHostData } from '../../../graphql/types'; +import { HostsOverviewStrategyResponse } from '../../../../common/search_strategy'; -export const mockData: { OverviewHost: OverviewHostData } = { - OverviewHost: { - auditbeatAuditd: 73847, - auditbeatFIM: 107307, - auditbeatLogin: 60015, - auditbeatPackage: 2003, - auditbeatProcess: 1200, - auditbeatUser: 1979, - endgameDns: 39123, - endgameFile: 39456, - endgameImageLoad: 39789, - endgameNetwork: 39101112, - endgameProcess: 39131415, - endgameRegistry: 39161718, - endgameSecurity: 39202122, - filebeatSystemModule: 568, - winlogbeatSecurity: 195929, - winlogbeatMWSysmonOperational: 101070, - }, +export const mockData: HostsOverviewStrategyResponse['overviewHost'] = { + auditbeatAuditd: 73847, + auditbeatFIM: 107307, + auditbeatLogin: 60015, + auditbeatPackage: 2003, + auditbeatProcess: 1200, + auditbeatUser: 1979, + endgameDns: 39123, + endgameFile: 39456, + endgameImageLoad: 39789, + endgameNetwork: 39101112, + endgameProcess: 39131415, + endgameRegistry: 39161718, + endgameSecurity: 39202122, + filebeatSystemModule: 568, + winlogbeatSecurity: 195929, + winlogbeatMWSysmonOperational: 101070, }; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network_stats/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network_stats/index.test.tsx index 0add7c1a02047..2f801ae1f3623 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network_stats/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network_stats/index.test.tsx @@ -14,9 +14,7 @@ import { TestProviders } from '../../../common/mock/test_providers'; describe('Overview Network Stat Data', () => { describe('rendering', () => { test('it renders the default OverviewNetworkStats', () => { - const wrapper = shallow( - - ); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); }); @@ -24,7 +22,7 @@ describe('Overview Network Stat Data', () => { test('it does NOT show loading indicator when loading is false', () => { const wrapper = mount( - + ); @@ -45,7 +43,7 @@ describe('Overview Network Stat Data', () => { test('it shows the loading indicator when loading is true', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network_stats/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network_stats/index.tsx index d3e16af7115ac..c6ad56b7243d4 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network_stats/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network_stats/index.tsx @@ -9,16 +9,18 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import styled from 'styled-components'; -import { OverviewNetworkData } from '../../../graphql/types'; +import { NetworkOverviewStrategyResponse } from '../../../../common/search_strategy'; import { FormattedStat, StatGroup } from '../types'; import { StatValue } from '../stat_value'; interface OverviewNetworkProps { - data: OverviewNetworkData; + data: NetworkOverviewStrategyResponse['overviewNetwork']; loading: boolean; } -export const getOverviewNetworkStats = (data: OverviewNetworkData): FormattedStat[] => [ +export const getOverviewNetworkStats = ( + data: NetworkOverviewStrategyResponse['overviewNetwork'] +): FormattedStat[] => [ { count: data.auditbeatSocket ?? 0, title: ( diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network_stats/mock.ts b/x-pack/plugins/security_solution/public/overview/components/overview_network_stats/mock.ts index f55d6a1577ccd..1eb337f1ea454 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network_stats/mock.ts +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network_stats/mock.ts @@ -4,18 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { OverviewNetworkData } from '../../../graphql/types'; +import { NetworkOverviewStrategyResponse } from '../../../../common/search_strategy'; -export const mockData: { OverviewNetwork: OverviewNetworkData } = { - OverviewNetwork: { - auditbeatSocket: 12, - filebeatCisco: 999, - filebeatNetflow: 7777, - filebeatPanw: 66, - filebeatSuricata: 60015, - filebeatZeek: 2003, - packetbeatDNS: 10277307, - packetbeatFlow: 16, - packetbeatTLS: 3400000, - }, +export const mockData: NetworkOverviewStrategyResponse['overviewNetwork'] = { + auditbeatSocket: 12, + filebeatCisco: 999, + filebeatNetflow: 7777, + filebeatPanw: 66, + filebeatSuricata: 60015, + filebeatZeek: 2003, + packetbeatDNS: 10277307, + packetbeatFlow: 16, + packetbeatTLS: 3400000, }; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.gql_query.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.gql_query.ts deleted file mode 100644 index 6f17bf6915aa4..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.gql_query.ts +++ /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; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const overviewHostQuery = gql` - query GetOverviewHostQuery( - $sourceId: ID! - $timerange: TimerangeInput! - $filterQuery: String - $defaultIndex: [String!]! - $inspect: Boolean! - ) { - source(id: $sourceId) { - id - OverviewHost(timerange: $timerange, filterQuery: $filterQuery, defaultIndex: $defaultIndex) { - auditbeatAuditd - auditbeatFIM - auditbeatLogin - auditbeatPackage - auditbeatProcess - auditbeatUser - endgameDns - endgameFile - endgameImageLoad - endgameNetwork - endgameProcess - endgameRegistry - endgameSecurity - filebeatSystemModule - winlogbeatSecurity - winlogbeatMWSysmonOperational - inspect @include(if: $inspect) { - dsl - response - } - } - } - } -`; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx index ac439107cb4a5..946cd33088a45 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx @@ -11,7 +11,7 @@ import deepEqual from 'fast-deep-equal'; import { HostsQueries, HostOverviewRequestOptions, - HostOverviewStrategyResponse, + HostsOverviewStrategyResponse, } from '../../../../common/search_strategy/security_solution'; import { useKibana } from '../../../common/lib/kibana'; import { inputsModel } from '../../../common/store/inputs'; @@ -32,7 +32,7 @@ export interface HostOverviewArgs { id: string; inspect: InspectResponse; isInspected: boolean; - overviewHost: HostOverviewStrategyResponse['overviewHost']; + overviewHost: HostsOverviewStrategyResponse['overviewHost']; refetch: inputsModel.Refetch; } @@ -85,7 +85,7 @@ export const useHostOverview = ({ setLoading(true); const searchSubscription$ = data.search - .search(request, { + .search(request, { strategy: 'securitySolutionSearchStrategy', abortSignal: abortCtrl.current.signal, }) diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.gql_query.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.gql_query.ts deleted file mode 100644 index d40ab900b91a7..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.gql_query.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const overviewNetworkQuery = gql` - query GetOverviewNetworkQuery( - $sourceId: ID! - $timerange: TimerangeInput! - $filterQuery: String - $defaultIndex: [String!]! - $inspect: Boolean! - ) { - source(id: $sourceId) { - id - OverviewNetwork( - timerange: $timerange - filterQuery: $filterQuery - defaultIndex: $defaultIndex - ) { - auditbeatSocket - filebeatCisco - filebeatNetflow - filebeatPanw - filebeatSuricata - filebeatZeek - packetbeatDNS - packetbeatFlow - packetbeatTLS - inspect @include(if: $inspect) { - dsl - response - } - } - } - } -`; diff --git a/x-pack/plugins/security_solution/server/graphql/index.ts b/x-pack/plugins/security_solution/server/graphql/index.ts index e949150c47c6c..2de6ef32b5703 100644 --- a/x-pack/plugins/security_solution/server/graphql/index.ts +++ b/x-pack/plugins/security_solution/server/graphql/index.ts @@ -15,7 +15,6 @@ import { ipDetailsSchemas } from './ip_details'; import { kpiHostsSchema } from './kpi_hosts'; import { kpiNetworkSchema } from './kpi_network'; import { networkSchema } from './network'; -import { overviewSchema } from './overview'; import { dateSchema } from './scalar_date'; import { noteSchema } from './note'; import { pinnedEventSchema } from './pinned_event'; @@ -44,7 +43,6 @@ export const schemas = [ matrixHistogramSchema, networkSchema, noteSchema, - overviewSchema, pinnedEventSchema, rootSchema, sourcesSchema, diff --git a/x-pack/plugins/security_solution/server/graphql/overview/index.ts b/x-pack/plugins/security_solution/server/graphql/overview/index.ts deleted file mode 100644 index 58cf182ccd976..0000000000000 --- a/x-pack/plugins/security_solution/server/graphql/overview/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { createOverviewResolvers } from './resolvers'; -export { overviewSchema } from './schema.gql'; diff --git a/x-pack/plugins/security_solution/server/graphql/overview/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/overview/resolvers.ts deleted file mode 100644 index a7bafabb64092..0000000000000 --- a/x-pack/plugins/security_solution/server/graphql/overview/resolvers.ts +++ /dev/null @@ -1,45 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SourceResolvers } from '../../graphql/types'; -import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; -import { Overview } from '../../lib/overview'; -import { createOptions } from '../../utils/build_query/create_options'; -import { QuerySourceResolver } from '../sources/resolvers'; - -export type QueryOverviewNetworkResolver = ChildResolverOf< - AppResolverOf, - QuerySourceResolver ->; - -export type QueryOverviewHostResolver = ChildResolverOf< - AppResolverOf, - QuerySourceResolver ->; - -export interface OverviewResolversDeps { - overview: Overview; -} - -export const createOverviewResolvers = ( - libs: OverviewResolversDeps -): { - Source: { - OverviewHost: QueryOverviewHostResolver; - OverviewNetwork: QueryOverviewNetworkResolver; - }; -} => ({ - Source: { - async OverviewNetwork(source, args, { req }, info) { - const options = { ...createOptions(source, args, info) }; - return libs.overview.getOverviewNetwork(req, options); - }, - async OverviewHost(source, args, { req }, info) { - const options = { ...createOptions(source, args, info) }; - return libs.overview.getOverviewHost(req, options); - }, - }, -}); diff --git a/x-pack/plugins/security_solution/server/graphql/overview/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/overview/schema.gql.ts deleted file mode 100644 index 7ab4f9fdb18d6..0000000000000 --- a/x-pack/plugins/security_solution/server/graphql/overview/schema.gql.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const overviewSchema = gql` - type OverviewNetworkData { - auditbeatSocket: Float - filebeatCisco: Float - filebeatNetflow: Float - filebeatPanw: Float - filebeatSuricata: Float - filebeatZeek: Float - packetbeatDNS: Float - packetbeatFlow: Float - packetbeatTLS: Float - inspect: Inspect - } - - type OverviewHostData { - auditbeatAuditd: Float - auditbeatFIM: Float - auditbeatLogin: Float - auditbeatPackage: Float - auditbeatProcess: Float - auditbeatUser: Float - endgameDns: Float - endgameFile: Float - endgameImageLoad: Float - endgameNetwork: Float - endgameProcess: Float - endgameRegistry: Float - endgameSecurity: Float - filebeatSystemModule: Float - winlogbeatSecurity: Float - winlogbeatMWSysmonOperational: Float - inspect: Inspect - } - - extend type Source { - OverviewNetwork( - id: String - timerange: TimerangeInput! - filterQuery: String - defaultIndex: [String!]! - ): OverviewNetworkData - OverviewHost( - id: String - timerange: TimerangeInput! - filterQuery: String - defaultIndex: [String!]! - ): OverviewHostData - } -`; diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 4c85c08e137fa..d10dfb16a9b8a 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -558,10 +558,6 @@ export interface Source { NetworkDnsHistogram: NetworkDsOverTimeData; NetworkHttp: NetworkHttpData; - - OverviewNetwork?: Maybe; - - OverviewHost?: Maybe; /** Just a simple example to get the app name */ whoAmI?: Maybe; } @@ -1834,64 +1830,6 @@ export interface NetworkHttpItem { statuses: string[]; } -export interface OverviewNetworkData { - auditbeatSocket?: Maybe; - - filebeatCisco?: Maybe; - - filebeatNetflow?: Maybe; - - filebeatPanw?: Maybe; - - filebeatSuricata?: Maybe; - - filebeatZeek?: Maybe; - - packetbeatDNS?: Maybe; - - packetbeatFlow?: Maybe; - - packetbeatTLS?: Maybe; - - inspect?: Maybe; -} - -export interface OverviewHostData { - auditbeatAuditd?: Maybe; - - auditbeatFIM?: Maybe; - - auditbeatLogin?: Maybe; - - auditbeatPackage?: Maybe; - - auditbeatProcess?: Maybe; - - auditbeatUser?: Maybe; - - endgameDns?: Maybe; - - endgameFile?: Maybe; - - endgameImageLoad?: Maybe; - - endgameNetwork?: Maybe; - - endgameProcess?: Maybe; - - endgameRegistry?: Maybe; - - endgameSecurity?: Maybe; - - filebeatSystemModule?: Maybe; - - winlogbeatSecurity?: Maybe; - - winlogbeatMWSysmonOperational?: Maybe; - - inspect?: Maybe; -} - export interface SayMyName { /** The id of the source */ appName: string; @@ -2489,24 +2427,6 @@ export interface NetworkHttpSourceArgs { defaultIndex: string[]; } -export interface OverviewNetworkSourceArgs { - id?: Maybe; - - timerange: TimerangeInput; - - filterQuery?: Maybe; - - defaultIndex: string[]; -} -export interface OverviewHostSourceArgs { - id?: Maybe; - - timerange: TimerangeInput; - - filterQuery?: Maybe; - - defaultIndex: string[]; -} export interface IndicesExistSourceStatusArgs { defaultIndex: string[]; } @@ -2943,10 +2863,6 @@ export namespace SourceResolvers { NetworkDnsHistogram?: NetworkDnsHistogramResolver; NetworkHttp?: NetworkHttpResolver; - - OverviewNetwork?: OverviewNetworkResolver, TypeParent, TContext>; - - OverviewHost?: OverviewHostResolver, TypeParent, TContext>; /** Just a simple example to get the app name */ whoAmI?: WhoAmIResolver, TypeParent, TContext>; } @@ -3298,36 +3214,6 @@ export namespace SourceResolvers { defaultIndex: string[]; } - export type OverviewNetworkResolver< - R = Maybe, - Parent = Source, - TContext = SiemContext - > = Resolver; - export interface OverviewNetworkArgs { - id?: Maybe; - - timerange: TimerangeInput; - - filterQuery?: Maybe; - - defaultIndex: string[]; - } - - export type OverviewHostResolver< - R = Maybe, - Parent = Source, - TContext = SiemContext - > = Resolver; - export interface OverviewHostArgs { - id?: Maybe; - - timerange: TimerangeInput; - - filterQuery?: Maybe; - - defaultIndex: string[]; - } - export type WhoAmIResolver< R = Maybe, Parent = Source, @@ -7598,209 +7484,6 @@ export namespace NetworkHttpItemResolvers { > = Resolver; } -export namespace OverviewNetworkDataResolvers { - export interface Resolvers { - auditbeatSocket?: AuditbeatSocketResolver, TypeParent, TContext>; - - filebeatCisco?: FilebeatCiscoResolver, TypeParent, TContext>; - - filebeatNetflow?: FilebeatNetflowResolver, TypeParent, TContext>; - - filebeatPanw?: FilebeatPanwResolver, TypeParent, TContext>; - - filebeatSuricata?: FilebeatSuricataResolver, TypeParent, TContext>; - - filebeatZeek?: FilebeatZeekResolver, TypeParent, TContext>; - - packetbeatDNS?: PacketbeatDnsResolver, TypeParent, TContext>; - - packetbeatFlow?: PacketbeatFlowResolver, TypeParent, TContext>; - - packetbeatTLS?: PacketbeatTlsResolver, TypeParent, TContext>; - - inspect?: InspectResolver, TypeParent, TContext>; - } - - export type AuditbeatSocketResolver< - R = Maybe, - Parent = OverviewNetworkData, - TContext = SiemContext - > = Resolver; - export type FilebeatCiscoResolver< - R = Maybe, - Parent = OverviewNetworkData, - TContext = SiemContext - > = Resolver; - export type FilebeatNetflowResolver< - R = Maybe, - Parent = OverviewNetworkData, - TContext = SiemContext - > = Resolver; - export type FilebeatPanwResolver< - R = Maybe, - Parent = OverviewNetworkData, - TContext = SiemContext - > = Resolver; - export type FilebeatSuricataResolver< - R = Maybe, - Parent = OverviewNetworkData, - TContext = SiemContext - > = Resolver; - export type FilebeatZeekResolver< - R = Maybe, - Parent = OverviewNetworkData, - TContext = SiemContext - > = Resolver; - export type PacketbeatDnsResolver< - R = Maybe, - Parent = OverviewNetworkData, - TContext = SiemContext - > = Resolver; - export type PacketbeatFlowResolver< - R = Maybe, - Parent = OverviewNetworkData, - TContext = SiemContext - > = Resolver; - export type PacketbeatTlsResolver< - R = Maybe, - Parent = OverviewNetworkData, - TContext = SiemContext - > = Resolver; - export type InspectResolver< - R = Maybe, - Parent = OverviewNetworkData, - TContext = SiemContext - > = Resolver; -} - -export namespace OverviewHostDataResolvers { - export interface Resolvers { - auditbeatAuditd?: AuditbeatAuditdResolver, TypeParent, TContext>; - - auditbeatFIM?: AuditbeatFimResolver, TypeParent, TContext>; - - auditbeatLogin?: AuditbeatLoginResolver, TypeParent, TContext>; - - auditbeatPackage?: AuditbeatPackageResolver, TypeParent, TContext>; - - auditbeatProcess?: AuditbeatProcessResolver, TypeParent, TContext>; - - auditbeatUser?: AuditbeatUserResolver, TypeParent, TContext>; - - endgameDns?: EndgameDnsResolver, TypeParent, TContext>; - - endgameFile?: EndgameFileResolver, TypeParent, TContext>; - - endgameImageLoad?: EndgameImageLoadResolver, TypeParent, TContext>; - - endgameNetwork?: EndgameNetworkResolver, TypeParent, TContext>; - - endgameProcess?: EndgameProcessResolver, TypeParent, TContext>; - - endgameRegistry?: EndgameRegistryResolver, TypeParent, TContext>; - - endgameSecurity?: EndgameSecurityResolver, TypeParent, TContext>; - - filebeatSystemModule?: FilebeatSystemModuleResolver, TypeParent, TContext>; - - winlogbeatSecurity?: WinlogbeatSecurityResolver, TypeParent, TContext>; - - winlogbeatMWSysmonOperational?: WinlogbeatMwSysmonOperationalResolver< - Maybe, - TypeParent, - TContext - >; - - inspect?: InspectResolver, TypeParent, TContext>; - } - - export type AuditbeatAuditdResolver< - R = Maybe, - Parent = OverviewHostData, - TContext = SiemContext - > = Resolver; - export type AuditbeatFimResolver< - R = Maybe, - Parent = OverviewHostData, - TContext = SiemContext - > = Resolver; - export type AuditbeatLoginResolver< - R = Maybe, - Parent = OverviewHostData, - TContext = SiemContext - > = Resolver; - export type AuditbeatPackageResolver< - R = Maybe, - Parent = OverviewHostData, - TContext = SiemContext - > = Resolver; - export type AuditbeatProcessResolver< - R = Maybe, - Parent = OverviewHostData, - TContext = SiemContext - > = Resolver; - export type AuditbeatUserResolver< - R = Maybe, - Parent = OverviewHostData, - TContext = SiemContext - > = Resolver; - export type EndgameDnsResolver< - R = Maybe, - Parent = OverviewHostData, - TContext = SiemContext - > = Resolver; - export type EndgameFileResolver< - R = Maybe, - Parent = OverviewHostData, - TContext = SiemContext - > = Resolver; - export type EndgameImageLoadResolver< - R = Maybe, - Parent = OverviewHostData, - TContext = SiemContext - > = Resolver; - export type EndgameNetworkResolver< - R = Maybe, - Parent = OverviewHostData, - TContext = SiemContext - > = Resolver; - export type EndgameProcessResolver< - R = Maybe, - Parent = OverviewHostData, - TContext = SiemContext - > = Resolver; - export type EndgameRegistryResolver< - R = Maybe, - Parent = OverviewHostData, - TContext = SiemContext - > = Resolver; - export type EndgameSecurityResolver< - R = Maybe, - Parent = OverviewHostData, - TContext = SiemContext - > = Resolver; - export type FilebeatSystemModuleResolver< - R = Maybe, - Parent = OverviewHostData, - TContext = SiemContext - > = Resolver; - export type WinlogbeatSecurityResolver< - R = Maybe, - Parent = OverviewHostData, - TContext = SiemContext - > = Resolver; - export type WinlogbeatMwSysmonOperationalResolver< - R = Maybe, - Parent = OverviewHostData, - TContext = SiemContext - > = Resolver; - export type InspectResolver< - R = Maybe, - Parent = OverviewHostData, - TContext = SiemContext - > = Resolver; -} - export namespace SayMyNameResolvers { export interface Resolvers { /** The id of the source */ @@ -9168,8 +8851,6 @@ export type IResolvers = { NetworkHttpData?: NetworkHttpDataResolvers.Resolvers; NetworkHttpEdges?: NetworkHttpEdgesResolvers.Resolvers; NetworkHttpItem?: NetworkHttpItemResolvers.Resolvers; - OverviewNetworkData?: OverviewNetworkDataResolvers.Resolvers; - OverviewHostData?: OverviewHostDataResolvers.Resolvers; SayMyName?: SayMyNameResolvers.Resolvers; TimelineResult?: TimelineResultResolvers.Resolvers; ColumnHeaderResult?: ColumnHeaderResultResolvers.Resolvers; diff --git a/x-pack/plugins/security_solution/server/init_server.ts b/x-pack/plugins/security_solution/server/init_server.ts index 7cb2127a3d9d7..ac0273ec1770d 100644 --- a/x-pack/plugins/security_solution/server/init_server.ts +++ b/x-pack/plugins/security_solution/server/init_server.ts @@ -16,7 +16,6 @@ import { createKpiNetworkResolvers } from './graphql/kpi_network'; import { createNetworkResolvers } from './graphql/network'; import { createNoteResolvers } from './graphql/note'; import { createPinnedEventResolvers } from './graphql/pinned_event'; -import { createOverviewResolvers } from './graphql/overview'; import { createScalarDateResolvers } from './graphql/scalar_date'; import { createScalarToAnyValueResolvers } from './graphql/scalar_to_any'; import { createScalarToBooleanArrayValueResolvers } from './graphql/scalar_to_boolean_array'; @@ -43,7 +42,6 @@ export const initServer = (libs: AppBackendLibs) => { createPinnedEventResolvers(libs) as IResolvers, createSourcesResolvers(libs) as IResolvers, createScalarToStringArrayValueResolvers() as IResolvers, - createOverviewResolvers(libs) as IResolvers, createNetworkResolvers(libs) as IResolvers, createScalarDateResolvers() as IResolvers, createScalarToDateArrayValueResolvers() as IResolvers, diff --git a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts index cfd7bfbf255f6..3bfb3d9492353 100644 --- a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts +++ b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts @@ -21,8 +21,6 @@ import { ElasticsearchIpDetailsAdapter, IpDetails } from '../ip_details'; import { KpiNetwork } from '../kpi_network'; import { ElasticsearchKpiNetworkAdapter } from '../kpi_network/elasticsearch_adapter'; import { ElasticsearchNetworkAdapter, Network } from '../network'; -import { Overview } from '../overview'; -import { ElasticsearchOverviewAdapter } from '../overview/elasticsearch_adapter'; import { ElasticsearchSourceStatusAdapter, SourceStatus } from '../source_status'; import { ConfigurationSourcesAdapter, Sources } from '../sources'; import { AppBackendLibs, AppDomainLibs } from '../types'; @@ -52,7 +50,6 @@ export function compose( kpiNetwork: new KpiNetwork(new ElasticsearchKpiNetworkAdapter(framework)), matrixHistogram: new MatrixHistogram(new ElasticsearchMatrixHistogramAdapter(framework)), network: new Network(new ElasticsearchNetworkAdapter(framework)), - overview: new Overview(new ElasticsearchOverviewAdapter(framework)), }; const libs: AppBackendLibs = { diff --git a/x-pack/plugins/security_solution/server/lib/overview/elastic_adapter.test.ts b/x-pack/plugins/security_solution/server/lib/overview/elastic_adapter.test.ts deleted file mode 100644 index f421704dffe12..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/overview/elastic_adapter.test.ts +++ /dev/null @@ -1,187 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { cloneDeep } from 'lodash/fp'; - -import { OverviewHostData, OverviewNetworkData } from '../../graphql/types'; -import { FrameworkAdapter, FrameworkRequest } from '../framework'; - -import { ElasticsearchOverviewAdapter } from './elasticsearch_adapter'; -import { - mockOptionsHost, - mockOptionsNetwork, - mockRequestHost, - mockRequestNetwork, - mockResponseHost, - mockResponseNetwork, - mockResultHost, - mockResultNetwork, - mockBuildOverviewHostQuery, - mockBuildOverviewNetworkQuery, -} from './mock'; - -jest.mock('./query.dsl', () => { - return { - buildOverviewHostQuery: jest.fn(() => mockBuildOverviewHostQuery), - buildOverviewNetworkQuery: jest.fn(() => mockBuildOverviewNetworkQuery), - }; -}); - -describe('Siem Overview elasticsearch_adapter', () => { - describe('Network Stats', () => { - describe('Happy Path - get Data', () => { - const mockCallWithRequest = jest.fn(); - mockCallWithRequest.mockResolvedValue(mockResponseNetwork); - const mockFramework: FrameworkAdapter = { - callWithRequest: mockCallWithRequest, - registerGraphQLEndpoint: jest.fn(), - getIndexPatternsService: jest.fn(), - }; - jest.doMock('../framework', () => ({ - callWithRequest: mockCallWithRequest, - })); - - test('getOverviewNetwork', async () => { - const EsOverviewNetwork = new ElasticsearchOverviewAdapter(mockFramework); - const data: OverviewNetworkData = await EsOverviewNetwork.getOverviewNetwork( - mockRequestNetwork as FrameworkRequest, - mockOptionsNetwork - ); - expect(data).toEqual(mockResultNetwork); - }); - }); - - describe('Unhappy Path - No data', () => { - const mockNoDataResponse = cloneDeep(mockResponseNetwork); - mockNoDataResponse.aggregations.unique_flow_count.doc_count = 0; - mockNoDataResponse.aggregations.unique_dns_count.doc_count = 0; - mockNoDataResponse.aggregations.unique_suricata_count.doc_count = 0; - mockNoDataResponse.aggregations.unique_zeek_count.doc_count = 0; - mockNoDataResponse.aggregations.unique_socket_count.doc_count = 0; - mockNoDataResponse.aggregations.unique_zeek_count.doc_count = 0; - mockNoDataResponse.aggregations.unique_packetbeat_count.unique_tls_count.doc_count = 0; - mockNoDataResponse.aggregations.unique_filebeat_count.unique_cisco_count.doc_count = 0; - mockNoDataResponse.aggregations.unique_filebeat_count.unique_netflow_count.doc_count = 0; - mockNoDataResponse.aggregations.unique_filebeat_count.unique_panw_count.doc_count = 0; - const mockCallWithRequest = jest.fn(); - mockCallWithRequest.mockResolvedValue(mockNoDataResponse); - const mockFramework: FrameworkAdapter = { - callWithRequest: mockCallWithRequest, - registerGraphQLEndpoint: jest.fn(), - getIndexPatternsService: jest.fn(), - }; - jest.doMock('../framework', () => ({ - callWithRequest: mockCallWithRequest, - })); - - test('getOverviewNetwork', async () => { - const EsOverviewNetwork = new ElasticsearchOverviewAdapter(mockFramework); - const data: OverviewNetworkData = await EsOverviewNetwork.getOverviewNetwork( - mockRequestNetwork as FrameworkRequest, - mockOptionsNetwork - ); - expect(data).toEqual({ - inspect: { - dsl: [JSON.stringify(mockBuildOverviewNetworkQuery, null, 2)], - response: [JSON.stringify(mockNoDataResponse, null, 2)], - }, - auditbeatSocket: 0, - filebeatCisco: 0, - filebeatNetflow: 0, - filebeatPanw: 0, - filebeatSuricata: 0, - filebeatZeek: 0, - packetbeatDNS: 0, - packetbeatFlow: 0, - packetbeatTLS: 0, - }); - }); - }); - }); - describe('Host Stats', () => { - describe('Happy Path - get Data', () => { - const mockCallWithRequest = jest.fn(); - mockCallWithRequest.mockResolvedValue(mockResponseHost); - const mockFramework: FrameworkAdapter = { - callWithRequest: mockCallWithRequest, - registerGraphQLEndpoint: jest.fn(), - getIndexPatternsService: jest.fn(), - }; - jest.doMock('../framework', () => ({ - callWithRequest: mockCallWithRequest, - })); - - test('getOverviewHost', async () => { - const EsOverviewHost = new ElasticsearchOverviewAdapter(mockFramework); - const data: OverviewHostData = await EsOverviewHost.getOverviewHost( - mockRequestHost as FrameworkRequest, - mockOptionsHost - ); - expect(data).toEqual(mockResultHost); - }); - }); - - describe('Unhappy Path - No data', () => { - const mockNoDataResponse = cloneDeep(mockResponseHost); - mockNoDataResponse.aggregations.auditd_count.doc_count = 0; - mockNoDataResponse.aggregations.endgame_module.dns_event_count.doc_count = 0; - mockNoDataResponse.aggregations.endgame_module.file_event_count.doc_count = 0; - mockNoDataResponse.aggregations.endgame_module.image_load_event_count.doc_count = 0; - mockNoDataResponse.aggregations.endgame_module.network_event_count.doc_count = 0; - mockNoDataResponse.aggregations.endgame_module.process_event_count.doc_count = 0; - mockNoDataResponse.aggregations.endgame_module.registry_event.doc_count = 0; - mockNoDataResponse.aggregations.endgame_module.security_event_count.doc_count = 0; - mockNoDataResponse.aggregations.fim_count.doc_count = 0; - mockNoDataResponse.aggregations.system_module.login_count.doc_count = 0; - mockNoDataResponse.aggregations.system_module.package_count.doc_count = 0; - mockNoDataResponse.aggregations.system_module.process_count.doc_count = 0; - mockNoDataResponse.aggregations.system_module.user_count.doc_count = 0; - mockNoDataResponse.aggregations.system_module.filebeat_count.doc_count = 0; - mockNoDataResponse.aggregations.winlog_module.security_event_count.doc_count = 0; - mockNoDataResponse.aggregations.winlog_module.mwsysmon_operational_event_count.doc_count = 0; - const mockCallWithRequest = jest.fn(); - mockCallWithRequest.mockResolvedValue(mockNoDataResponse); - const mockFramework: FrameworkAdapter = { - callWithRequest: mockCallWithRequest, - registerGraphQLEndpoint: jest.fn(), - getIndexPatternsService: jest.fn(), - }; - jest.doMock('../framework', () => ({ - callWithRequest: mockCallWithRequest, - })); - - test('getOverviewHost', async () => { - const EsOverviewHost = new ElasticsearchOverviewAdapter(mockFramework); - const data: OverviewHostData = await EsOverviewHost.getOverviewHost( - mockRequestHost as FrameworkRequest, - mockOptionsHost - ); - expect(data).toEqual({ - inspect: { - dsl: [JSON.stringify(mockBuildOverviewHostQuery, null, 2)], - response: [JSON.stringify(mockNoDataResponse, null, 2)], - }, - auditbeatAuditd: 0, - auditbeatFIM: 0, - auditbeatLogin: 0, - auditbeatPackage: 0, - auditbeatProcess: 0, - auditbeatUser: 0, - endgameDns: 0, - endgameFile: 0, - endgameImageLoad: 0, - endgameNetwork: 0, - endgameProcess: 0, - endgameRegistry: 0, - endgameSecurity: 0, - filebeatSystemModule: 0, - winlogbeatSecurity: 0, - winlogbeatMWSysmonOperational: 0, - }); - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/overview/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/overview/elasticsearch_adapter.ts deleted file mode 100644 index 982b47110c513..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/overview/elasticsearch_adapter.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; - -import { OverviewHostData, OverviewNetworkData } from '../../graphql/types'; -import { inspectStringifyObject } from '../../utils/build_query'; -import { FrameworkAdapter, FrameworkRequest, RequestBasicOptions } from '../framework'; -import { TermAggregation } from '../types'; - -import { buildOverviewHostQuery, buildOverviewNetworkQuery } from './query.dsl'; -import { OverviewAdapter, OverviewHostHit, OverviewNetworkHit } from './types'; - -export class ElasticsearchOverviewAdapter implements OverviewAdapter { - constructor(private readonly framework: FrameworkAdapter) {} - - public async getOverviewNetwork( - request: FrameworkRequest, - options: RequestBasicOptions - ): Promise { - const dsl = buildOverviewNetworkQuery(options); - const response = await this.framework.callWithRequest( - request, - 'search', - dsl - ); - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - - return { - inspect, - auditbeatSocket: getOr(null, 'aggregations.unique_socket_count.doc_count', response), - filebeatCisco: getOr( - null, - 'aggregations.unique_filebeat_count.unique_cisco_count.doc_count', - response - ), - filebeatNetflow: getOr( - null, - 'aggregations.unique_filebeat_count.unique_netflow_count.doc_count', - response - ), - filebeatPanw: getOr( - null, - 'aggregations.unique_filebeat_count.unique_panw_count.doc_count', - response - ), - filebeatSuricata: getOr(null, 'aggregations.unique_suricata_count.doc_count', response), - filebeatZeek: getOr(null, 'aggregations.unique_zeek_count.doc_count', response), - packetbeatDNS: getOr(null, 'aggregations.unique_dns_count.doc_count', response), - packetbeatFlow: getOr(null, 'aggregations.unique_flow_count.doc_count', response), - packetbeatTLS: getOr( - null, - 'aggregations.unique_packetbeat_count.unique_tls_count.doc_count', - response - ), - }; - } - - public async getOverviewHost( - request: FrameworkRequest, - options: RequestBasicOptions - ): Promise { - const dsl = buildOverviewHostQuery(options); - const response = await this.framework.callWithRequest( - request, - 'search', - dsl - ); - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - - return { - inspect, - auditbeatAuditd: getOr(null, 'aggregations.auditd_count.doc_count', response), - auditbeatFIM: getOr(null, 'aggregations.fim_count.doc_count', response), - auditbeatLogin: getOr(null, 'aggregations.system_module.login_count.doc_count', response), - auditbeatPackage: getOr(null, 'aggregations.system_module.package_count.doc_count', response), - auditbeatProcess: getOr(null, 'aggregations.system_module.process_count.doc_count', response), - auditbeatUser: getOr(null, 'aggregations.system_module.user_count.doc_count', response), - endgameDns: getOr(null, 'aggregations.endgame_module.dns_event_count.doc_count', response), - endgameFile: getOr(null, 'aggregations.endgame_module.file_event_count.doc_count', response), - endgameImageLoad: getOr( - null, - 'aggregations.endgame_module.image_load_event_count.doc_count', - response - ), - endgameNetwork: getOr( - null, - 'aggregations.endgame_module.network_event_count.doc_count', - response - ), - endgameProcess: getOr( - null, - 'aggregations.endgame_module.process_event_count.doc_count', - response - ), - endgameRegistry: getOr( - null, - 'aggregations.endgame_module.registry_event.doc_count', - response - ), - endgameSecurity: getOr( - null, - 'aggregations.endgame_module.security_event_count.doc_count', - response - ), - filebeatSystemModule: getOr( - null, - 'aggregations.system_module.filebeat_count.doc_count', - response - ), - winlogbeatSecurity: getOr( - null, - 'aggregations.winlog_module.security_event_count.doc_count', - response - ), - winlogbeatMWSysmonOperational: getOr( - null, - 'aggregations.winlog_module.mwsysmon_operational_event_count.doc_count', - response - ), - }; - } -} diff --git a/x-pack/plugins/security_solution/server/lib/overview/index.ts b/x-pack/plugins/security_solution/server/lib/overview/index.ts deleted file mode 100644 index ae9f81eb261a7..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/overview/index.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { OverviewHostData, OverviewNetworkData } from '../../graphql/types'; -import { FrameworkRequest, RequestBasicOptions } from '../framework'; - -import { OverviewAdapter } from './types'; - -export class Overview { - constructor(private readonly adapter: OverviewAdapter) {} - - public async getOverviewNetwork( - req: FrameworkRequest, - options: RequestBasicOptions - ): Promise { - return this.adapter.getOverviewNetwork(req, options); - } - - public async getOverviewHost( - req: FrameworkRequest, - options: RequestBasicOptions - ): Promise { - return this.adapter.getOverviewHost(req, options); - } -} diff --git a/x-pack/plugins/security_solution/server/lib/overview/mock.ts b/x-pack/plugins/security_solution/server/lib/overview/mock.ts deleted file mode 100644 index 2621c795ecd6b..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/overview/mock.ts +++ /dev/null @@ -1,176 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { DEFAULT_INDEX_PATTERN } from '../../../common/constants'; -import { RequestBasicOptions } from '../framework/types'; - -export const mockOptionsNetwork: RequestBasicOptions = { - defaultIndex: DEFAULT_INDEX_PATTERN, - sourceConfiguration: { - fields: { - container: 'docker.container.name', - host: 'beat.hostname', - message: ['message', '@message'], - pod: 'kubernetes.pod.name', - tiebreaker: '_doc', - timestamp: '@timestamp', - }, - }, - timerange: { interval: '12h', to: '2019-02-11T02:26:46.071Z', from: '2019-02-10T02:26:46.071Z' }, - filterQuery: {}, -}; - -export const mockRequestNetwork = { - body: { - operationName: 'GetOverviewNetworkQuery', - variables: { - sourceId: 'default', - timerange: { - interval: '12h', - from: '2019-02-10T02:30:30.772Z', - to: '2019-02-11T02:30:30.772Z', - }, - filterQuery: '', - }, - query: - 'query GetOverviewNetworkQuery(\n $sourceId: ID!\n $timerange: TimerangeInput!\n $filterQuery: String\n ) {\n source(id: $sourceId) {\n id\n OverviewNetwork(timerange: $timerange, filterQuery: $filterQuery) {\n packetbeatFlow\n packetbeatDNS\n filebeatSuricata\n filebeatZeek\n auditbeatSocket\n }\n }\n }', - }, -}; - -export const mockResponseNetwork = { - took: 89, - timed_out: false, - _shards: { total: 18, successful: 18, skipped: 0, failed: 0 }, - hits: { total: { value: 950867, relation: 'eq' }, max_score: null, hits: [] }, - aggregations: { - unique_flow_count: { doc_count: 50243 }, - unique_dns_count: { doc_count: 15000 }, - unique_suricata_count: { doc_count: 2375 }, - unique_zeek_count: { doc_count: 456 }, - unique_socket_count: { doc_count: 13 }, - unique_filebeat_count: { - doc_count: 456756, - unique_cisco_count: { doc_count: 14 }, - unique_netflow_count: { doc_count: 992 }, - unique_panw_count: { doc_count: 225 }, - }, - unique_packetbeat_count: { doc_count: 7897896, unique_tls_count: { doc_count: 2009 } }, - }, -}; - -export const mockBuildOverviewHostQuery = { buildOverviewHostQuery: 'buildOverviewHostQuery' }; -export const mockBuildOverviewNetworkQuery = { - buildOverviewNetworkQuery: 'buildOverviewNetworkQuery', -}; - -export const mockResultNetwork = { - inspect: { - dsl: [JSON.stringify(mockBuildOverviewNetworkQuery, null, 2)], - response: [JSON.stringify(mockResponseNetwork, null, 2)], - }, - packetbeatFlow: 50243, - packetbeatDNS: 15000, - filebeatSuricata: 2375, - filebeatZeek: 456, - auditbeatSocket: 13, - filebeatCisco: 14, - filebeatNetflow: 992, - filebeatPanw: 225, - packetbeatTLS: 2009, -}; - -export const mockOptionsHost: RequestBasicOptions = { - defaultIndex: DEFAULT_INDEX_PATTERN, - sourceConfiguration: { - fields: { - container: 'docker.container.name', - host: 'beat.hostname', - message: ['message', '@message'], - pod: 'kubernetes.pod.name', - tiebreaker: '_doc', - timestamp: '@timestamp', - }, - }, - timerange: { interval: '12h', to: '2019-02-11T02:26:46.071Z', from: '2019-02-10T02:26:46.071Z' }, - filterQuery: {}, -}; - -export const mockRequestHost = { - body: { - operationName: 'GetOverviewHostQuery', - variables: { - sourceId: 'default', - timerange: { - interval: '12h', - from: '2019-02-10T02:30:30.772Z', - to: '2019-02-11T02:30:30.772Z', - }, - filterQuery: '', - }, - query: - 'query GetOverviewHostQuery(\n $sourceId: ID!\n $timerange: TimerangeInput!\n $filterQuery: String\n ) {\n source(id: $sourceId) {\n id\n OverviewHost(timerange: $timerange, filterQuery: $filterQuery) {\n auditbeatAuditd\n auditbeatFIM\n auditbeatLogin\n auditbeatPackage\n auditbeatProcess\n auditbeatUser\n }\n }\n }', - }, -}; - -export const mockResponseHost = { - took: 89, - timed_out: false, - _shards: { total: 18, successful: 18, skipped: 0, failed: 0 }, - hits: { total: { value: 950867, relation: 'eq' }, max_score: null, hits: [] }, - aggregations: { - auditd_count: { doc_count: 73847 }, - endgame_module: { - doc_count: 6258, - dns_event_count: { doc_count: 891 }, - file_event_count: { doc_count: 892 }, - image_load_event_count: { doc_count: 893 }, - network_event_count: { doc_count: 894 }, - process_event_count: { doc_count: 895 }, - registry_event: { doc_count: 896 }, - security_event_count: { doc_count: 897 }, - }, - fim_count: { doc_count: 107307 }, - system_module: { - doc_count: 20000000, - login_count: { doc_count: 60015 }, - package_count: { doc_count: 2003 }, - process_count: { doc_count: 1200 }, - user_count: { doc_count: 1979 }, - filebeat_count: { doc_count: 225 }, - }, - winlog_module: { - security_event_count: { - doc_count: 523, - }, - mwsysmon_operational_event_count: { - doc_count: 214, - }, - }, - }, -}; - -export const mockResultHost = { - inspect: { - dsl: [JSON.stringify(mockBuildOverviewHostQuery, null, 2)], - response: [JSON.stringify(mockResponseHost, null, 2)], - }, - auditbeatAuditd: 73847, - auditbeatFIM: 107307, - auditbeatLogin: 60015, - auditbeatPackage: 2003, - auditbeatProcess: 1200, - auditbeatUser: 1979, - endgameDns: 891, - endgameFile: 892, - endgameImageLoad: 893, - endgameNetwork: 894, - endgameProcess: 895, - endgameRegistry: 896, - endgameSecurity: 897, - filebeatSystemModule: 225, - winlogbeatSecurity: 523, - winlogbeatMWSysmonOperational: 214, -}; diff --git a/x-pack/plugins/security_solution/server/lib/overview/query.dsl.ts b/x-pack/plugins/security_solution/server/lib/overview/query.dsl.ts deleted file mode 100644 index b6b1cfea394fd..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/overview/query.dsl.ts +++ /dev/null @@ -1,397 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import { createQueryFilterClauses } from '../../utils/build_query'; -import { RequestBasicOptions } from '../framework'; - -export const buildOverviewNetworkQuery = ({ - filterQuery, - timerange: { from, to }, - defaultIndex, - sourceConfiguration: { - fields: { timestamp }, - }, -}: RequestBasicOptions) => { - const filter = [ - ...createQueryFilterClauses(filterQuery), - { - range: { - [timestamp]: { - gte: from, - lte: to, - format: 'strict_date_optional_time', - }, - }, - }, - ]; - - const dslQuery = { - allowNoIndices: true, - index: defaultIndex, - ignoreUnavailable: true, - body: { - aggregations: { - unique_flow_count: { - filter: { - term: { type: 'flow' }, - }, - }, - unique_dns_count: { - filter: { - term: { type: 'dns' }, - }, - }, - unique_suricata_count: { - filter: { - term: { 'service.type': 'suricata' }, - }, - }, - unique_zeek_count: { - filter: { - term: { 'service.type': 'zeek' }, - }, - }, - unique_socket_count: { - filter: { - term: { 'event.dataset': 'socket' }, - }, - }, - unique_filebeat_count: { - filter: { - term: { 'agent.type': 'filebeat' }, - }, - aggs: { - unique_netflow_count: { - filter: { - term: { 'input.type': 'netflow' }, - }, - }, - unique_panw_count: { - filter: { - term: { 'event.module': 'panw' }, - }, - }, - unique_cisco_count: { - filter: { - term: { 'event.module': 'cisco' }, - }, - }, - }, - }, - unique_packetbeat_count: { - filter: { - term: { 'agent.type': 'packetbeat' }, - }, - aggs: { - unique_tls_count: { - filter: { - term: { 'network.protocol': 'tls' }, - }, - }, - }, - }, - }, - query: { - bool: { - filter, - }, - }, - size: 0, - track_total_hits: false, - }, - }; - - return dslQuery; -}; - -export const buildOverviewHostQuery = ({ - filterQuery, - timerange: { from, to }, - defaultIndex, - sourceConfiguration: { - fields: { timestamp }, - }, -}: RequestBasicOptions) => { - const filter = [ - ...createQueryFilterClauses(filterQuery), - { - range: { - [timestamp]: { - gte: from, - lte: to, - format: 'strict_date_optional_time', - }, - }, - }, - ]; - - const dslQuery = { - allowNoIndices: true, - index: defaultIndex, - ignoreUnavailable: true, - body: { - aggregations: { - auditd_count: { - filter: { - term: { - 'event.module': 'auditd', - }, - }, - }, - endgame_module: { - filter: { - bool: { - should: [ - { - term: { 'event.module': 'endpoint' }, - }, - { - term: { - 'event.module': 'endgame', - }, - }, - ], - }, - }, - aggs: { - dns_event_count: { - filter: { - bool: { - should: [ - { - bool: { - filter: [ - { term: { 'network.protocol': 'dns' } }, - { term: { 'event.category': 'network' } }, - ], - }, - }, - { - term: { - 'endgame.event_type_full': 'dns_event', - }, - }, - ], - }, - }, - }, - file_event_count: { - filter: { - bool: { - should: [ - { - term: { - 'event.category': 'file', - }, - }, - { - term: { - 'endgame.event_type_full': 'file_event', - }, - }, - ], - }, - }, - }, - image_load_event_count: { - filter: { - bool: { - should: [ - { - bool: { - should: [ - { - term: { - 'event.category': 'library', - }, - }, - { - term: { - 'event.category': 'driver', - }, - }, - ], - }, - }, - { - term: { - 'endgame.event_type_full': 'image_load_event', - }, - }, - ], - }, - }, - }, - network_event_count: { - filter: { - bool: { - should: [ - { - bool: { - filter: [ - { - bool: { - must_not: { - term: { 'network.protocol': 'dns' }, - }, - }, - }, - { - term: { 'event.category': 'network' }, - }, - ], - }, - }, - { - term: { - 'endgame.event_type_full': 'network_event', - }, - }, - ], - }, - }, - }, - process_event_count: { - filter: { - bool: { - should: [ - { - term: { 'event.category': 'process' }, - }, - { - term: { - 'endgame.event_type_full': 'process_event', - }, - }, - ], - }, - }, - }, - registry_event: { - filter: { - bool: { - should: [ - { - term: { 'event.category': 'registry' }, - }, - { - term: { - 'endgame.event_type_full': 'registry_event', - }, - }, - ], - }, - }, - }, - security_event_count: { - filter: { - bool: { - should: [ - { - bool: { - filter: [ - { term: { 'event.category': 'session' } }, - { term: { 'event.category': 'authentication' } }, - ], - }, - }, - { - term: { - 'endgame.event_type_full': 'security_event', - }, - }, - ], - }, - }, - }, - }, - }, - fim_count: { - filter: { - term: { - 'event.module': 'file_integrity', - }, - }, - }, - winlog_module: { - filter: { - term: { - 'agent.type': 'winlogbeat', - }, - }, - aggs: { - mwsysmon_operational_event_count: { - filter: { - term: { - 'winlog.channel': 'Microsoft-Windows-Sysmon/Operational', - }, - }, - }, - security_event_count: { - filter: { - term: { - 'winlog.channel': 'Security', - }, - }, - }, - }, - }, - system_module: { - filter: { - term: { - 'event.module': 'system', - }, - }, - aggs: { - login_count: { - filter: { - term: { - 'event.dataset': 'login', - }, - }, - }, - package_count: { - filter: { - term: { - 'event.dataset': 'package', - }, - }, - }, - process_count: { - filter: { - term: { - 'event.dataset': 'process', - }, - }, - }, - user_count: { - filter: { - term: { - 'event.dataset': 'user', - }, - }, - }, - filebeat_count: { - filter: { - term: { - 'agent.type': 'filebeat', - }, - }, - }, - }, - }, - }, - query: { - bool: { - filter, - }, - }, - size: 0, - track_total_hits: false, - }, - }; - - return dslQuery; -}; diff --git a/x-pack/plugins/security_solution/server/lib/overview/types.ts b/x-pack/plugins/security_solution/server/lib/overview/types.ts deleted file mode 100644 index 7fdad08ac9b37..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/overview/types.ts +++ /dev/null @@ -1,109 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import { OverviewHostData, OverviewNetworkData } from '../../graphql/types'; -import { FrameworkRequest, RequestBasicOptions } from '../framework'; -import { SearchHit } from '../types'; - -export interface OverviewAdapter { - getOverviewNetwork( - request: FrameworkRequest, - options: RequestBasicOptions - ): Promise; - getOverviewHost( - request: FrameworkRequest, - options: RequestBasicOptions - ): Promise; -} - -export interface OverviewNetworkHit extends SearchHit { - aggregations: { - unique_flow_count: { - doc_count: number; - }; - unique_dns_count: { - doc_count: number; - }; - unique_suricata_count: { - doc_count: number; - }; - unique_zeek_count: { - doc_count: number; - }; - unique_socket_count: { - doc_count: number; - }; - unique_filebeat_count: { - unique_netflow_count: { - doc_count: number; - }; - unique_panw_count: { - doc_count: number; - }; - unique_cisco_count: { - doc_count: number; - }; - }; - unique_packetbeat_count: { - unique_tls_count: { - doc_count: number; - }; - }; - }; -} - -export interface OverviewHostHit extends SearchHit { - aggregations: { - auditd_count: { - doc_count: number; - }; - endgame_module: { - dns_event_count: { - doc_count: number; - }; - file_event_count: { - doc_count: number; - }; - image_load_event_count: { - doc_count: number; - }; - network_event_count: { - doc_count: number; - }; - process_event_count: { - doc_count: number; - }; - registry_event: { - doc_count: number; - }; - security_event_count: { - doc_count: number; - }; - }; - fim_count: { - doc_count: number; - }; - system_module: { - login_count: { - doc_count: number; - }; - package_count: { - doc_count: number; - }; - process_count: { - doc_count: number; - }; - user_count: { - doc_count: number; - }; - filebeat_count: { - doc_count: number; - }; - }; - winlog_count: { - doc_count: number; - }; - }; -} diff --git a/x-pack/plugins/security_solution/server/lib/types.ts b/x-pack/plugins/security_solution/server/lib/types.ts index 87e755360285f..3c7c1cd3d7cff 100644 --- a/x-pack/plugins/security_solution/server/lib/types.ts +++ b/x-pack/plugins/security_solution/server/lib/types.ts @@ -17,7 +17,6 @@ import { IpDetails } from './ip_details'; import { KpiHosts } from './kpi_hosts'; import { KpiNetwork } from './kpi_network'; import { Network } from './network'; -import { Overview } from './overview'; import { SourceStatus } from './source_status'; import { Sources } from './sources'; import { Note } from './note/saved_object'; @@ -36,7 +35,6 @@ export interface AppDomainLibs { matrixHistogram: MatrixHistogram; network: Network; kpiNetwork: KpiNetwork; - overview: Overview; kpiHosts: KpiHosts; } diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts index 7a28c983ec466..61c228a5fd164 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/overview/index.ts @@ -8,7 +8,7 @@ import { get, getOr } from 'lodash/fp'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; import { - HostOverviewStrategyResponse, + HostsOverviewStrategyResponse, HostsQueries, HostOverviewRequestOptions, OverviewHostHit, @@ -22,7 +22,7 @@ export const hostOverview: SecuritySolutionFactory = { parse: async ( options: HostOverviewRequestOptions, response: IEsSearchResponse - ): Promise => { + ): Promise => { const aggregations: OverviewHostHit = get('aggregations', response.rawResponse) || {}; const inspect = { dsl: [inspectStringifyObject(buildOverviewHostQuery(options))], diff --git a/x-pack/test/api_integration/apis/security_solution/index.js b/x-pack/test/api_integration/apis/security_solution/index.js index 16a38c0fafbca..a9ddf091245f7 100644 --- a/x-pack/test/api_integration/apis/security_solution/index.js +++ b/x-pack/test/api_integration/apis/security_solution/index.js @@ -12,12 +12,12 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./kpi_hosts')); loadTestFile(require.resolve('./network_dns')); loadTestFile(require.resolve('./network_top_n_flow')); - loadTestFile(require.resolve('./overview_host')); + // loadTestFile(require.resolve('./overview_host')); loadTestFile(require.resolve('./saved_objects/notes')); loadTestFile(require.resolve('./saved_objects/pinned_events')); loadTestFile(require.resolve('./saved_objects/timeline')); loadTestFile(require.resolve('./sources')); - loadTestFile(require.resolve('./overview_network')); + // loadTestFile(require.resolve('./overview_network')); loadTestFile(require.resolve('./timeline')); loadTestFile(require.resolve('./timeline_details')); // loadTestFile(require.resolve('./uncommon_processes')); diff --git a/x-pack/test/api_integration/apis/security_solution/overview_host.ts b/x-pack/test/api_integration/apis/security_solution/overview_host.ts index ffbf9d89fc112..0d648e665a9a9 100644 --- a/x-pack/test/api_integration/apis/security_solution/overview_host.ts +++ b/x-pack/test/api_integration/apis/security_solution/overview_host.ts @@ -7,7 +7,9 @@ import expect from '@kbn/expect'; import { DEFAULT_INDEX_PATTERN } from '../../../../plugins/security_solution/common/constants'; +// @ts-expect-error import { overviewHostQuery } from '../../../../plugins/security_solution/public/overview/containers//overview_host/index.gql_query'; +// @ts-expect-error import { GetOverviewHostQuery } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/apis/security_solution/overview_network.ts b/x-pack/test/api_integration/apis/security_solution/overview_network.ts index 6976b225a4d2a..60d300e168e4a 100644 --- a/x-pack/test/api_integration/apis/security_solution/overview_network.ts +++ b/x-pack/test/api_integration/apis/security_solution/overview_network.ts @@ -5,7 +5,9 @@ */ import expect from '@kbn/expect'; +// @ts-expect-error import { overviewNetworkQuery } from '../../../../plugins/security_solution/public/overview/containers/overview_network/index.gql_query'; +// @ts-expect-error import { GetOverviewNetworkQuery } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; From a00b3ee671b5cbb22b9efc6afb8d589744063874 Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Wed, 23 Sep 2020 16:49:26 -0400 Subject: [PATCH 68/92] [Ingest Manager] add upgrade action (#77412) * at agents/agent-id/upgrade endpoint * handle upgrade ack * let upgrade endpoint accept url and version * wrap action data in data prop * decrypt data of actions * type upgrade action and decrypt data in ack * error if trying to update to diff version of kibana * add some integration tests * untype * fix test * update integration test * reset upgraded_at when upgrading * use defaultIngestErrorHandler * use ack_data instead of data * copy data to ack_data --- .../ingest_manager/common/constants/routes.ts | 1 + .../common/services/agent_status.ts | 3 + .../common/types/models/agent.ts | 7 ++- .../common/types/rest_spec/agent.ts | 7 +++ .../server/routes/agent/index.ts | 12 +++- .../server/routes/agent/upgrade_handler.ts | 47 ++++++++++++++ .../server/saved_objects/index.ts | 2 + .../server/services/agents/acks.ts | 6 ++ .../server/services/agents/actions.ts | 35 +++++++++-- .../server/services/agents/index.ts | 1 + .../server/services/agents/upgrade.ts | 61 +++++++++++++++++++ .../server/types/models/agent.ts | 7 ++- .../server/types/rest_spec/agent.ts | 10 +++ .../apis/fleet/agents/acks.ts | 46 ++++++++++++++ .../apis/fleet/agents/upgrade.ts | 57 +++++++++++++++++ .../apis/fleet/index.js | 1 + 16 files changed, 292 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/routes/agent/upgrade_handler.ts create mode 100644 x-pack/plugins/ingest_manager/server/services/agents/upgrade.ts create mode 100644 x-pack/test/ingest_manager_api_integration/apis/fleet/agents/upgrade.ts diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index d899739a74ef0..69672dfb9ec6c 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -90,6 +90,7 @@ export const AGENT_API_ROUTES = { REASSIGN_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/reassign`, BULK_REASSIGN_PATTERN: `${FLEET_API_ROOT}/agents/bulk_reassign`, STATUS_PATTERN: `${FLEET_API_ROOT}/agent-status`, + UPGRADE_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/upgrade`, }; export const ENROLLMENT_API_KEY_ROUTES = { diff --git a/x-pack/plugins/ingest_manager/common/services/agent_status.ts b/x-pack/plugins/ingest_manager/common/services/agent_status.ts index fe4e094e1bb22..70f4d7f9344f9 100644 --- a/x-pack/plugins/ingest_manager/common/services/agent_status.ts +++ b/x-pack/plugins/ingest_manager/common/services/agent_status.ts @@ -19,6 +19,9 @@ export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentSta if (!agent.last_checkin) { return 'enrolling'; } + if (agent.upgrade_started_at && !agent.upgraded_at) { + return 'upgrading'; + } const msLastCheckIn = new Date(lastCheckIn || 0).getTime(); const msSinceLastCheckIn = new Date().getTime() - msLastCheckIn; diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts index a204373fe2e56..7110fd4ce52ea 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts @@ -19,10 +19,10 @@ export type AgentStatus = | 'warning' | 'enrolling' | 'unenrolling' + | 'upgrading' | 'degraded'; -export type AgentActionType = 'CONFIG_CHANGE' | 'UNENROLL'; - +export type AgentActionType = 'CONFIG_CHANGE' | 'UNENROLL' | 'UPGRADE'; export interface NewAgentAction { type: AgentActionType; data?: any; @@ -65,7 +65,6 @@ export type AgentPolicyActionSOAttributes = CommonAgentActionSOAttributes & { policy_id: string; policy_revision: number; }; - export type BaseAgentActionSOAttributes = AgentActionSOAttributes | AgentPolicyActionSOAttributes; export interface NewAgentEvent { @@ -110,6 +109,8 @@ interface AgentBase { enrolled_at: string; unenrolled_at?: string; unenrollment_started_at?: string; + upgraded_at?: string; + upgrade_started_at?: string; shared_id?: string; access_api_key_id?: string; default_api_key?: string; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts index 1a10d4930656f..ab4c372c4e1d6 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts @@ -113,6 +113,11 @@ export interface PostAgentUnenrollRequest { // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PostAgentUnenrollResponse {} +export interface PostAgentUpgradeRequest { + params: { + agentId: string; + }; +} export interface PostBulkAgentUnenrollRequest { body: { agents: string[] | string; @@ -120,6 +125,8 @@ export interface PostBulkAgentUnenrollRequest { }; } +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PostAgentUpgradeResponse {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PostBulkAgentUnenrollResponse {} diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts index eafc726ea166d..73ed276ba02e7 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts @@ -29,6 +29,7 @@ import { PutAgentReassignRequestSchema, PostBulkAgentReassignRequestSchema, PostAgentEnrollRequestBodyJSONSchema, + PostAgentUpgradeRequestSchema, } from '../../types'; import { getAgentsHandler, @@ -48,6 +49,7 @@ import { postNewAgentActionHandlerBuilder } from './actions_handlers'; import { appContextService } from '../../services'; import { postAgentUnenrollHandler, postBulkAgentsUnenrollHandler } from './unenroll_handler'; import { IngestManagerConfigType } from '../..'; +import { postAgentUpgradeHandler } from './upgrade_handler'; const ajv = new Ajv({ coerceTypes: true, @@ -215,7 +217,15 @@ export const registerRoutes = (router: IRouter, config: IngestManagerConfigType) }, getAgentStatusForAgentPolicyHandler ); - + // upgrade agent + router.post( + { + path: AGENT_API_ROUTES.UPGRADE_PATTERN, + validate: PostAgentUpgradeRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + postAgentUpgradeHandler + ); // Bulk reassign router.post( { diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/upgrade_handler.ts b/x-pack/plugins/ingest_manager/server/routes/agent/upgrade_handler.ts new file mode 100644 index 0000000000000..e5d7a44c00768 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/agent/upgrade_handler.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RequestHandler } from 'src/core/server'; +import { TypeOf } from '@kbn/config-schema'; +import { PostAgentUpgradeResponse } from '../../../common/types'; +import { PostAgentUpgradeRequestSchema } from '../../types'; +import * as AgentService from '../../services/agents'; +import { appContextService } from '../../services'; +import { defaultIngestErrorHandler } from '../../errors'; + +export const postAgentUpgradeHandler: RequestHandler< + TypeOf, + undefined, + TypeOf +> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + const { version, source_uri: sourceUri } = request.body; + + // temporarily only allow upgrading to the same version as the installed kibana version + const kibanaVersion = appContextService.getKibanaVersion(); + if (kibanaVersion !== version) { + return response.customError({ + statusCode: 400, + body: { + message: `cannot upgrade agent to ${version} because it is different than the installed kibana version ${kibanaVersion}`, + }, + }); + } + + try { + await AgentService.sendUpgradeAgentAction({ + soClient, + agentId: request.params.agentId, + version, + sourceUri, + }); + + const body: PostAgentUpgradeResponse = {}; + return response.ok({ body }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } +}; diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts index e86f7b24e2c78..fd08b76a3916b 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts @@ -68,6 +68,8 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { enrolled_at: { type: 'date' }, unenrolled_at: { type: 'date' }, unenrollment_started_at: { type: 'date' }, + upgraded_at: { type: 'date' }, + upgrade_started_at: { type: 'date' }, access_api_key_id: { type: 'keyword' }, version: { type: 'keyword' }, user_provided_metadata: { type: 'flattened' }, diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts index 1392710eb0eff..e22ee4256b0e2 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts @@ -28,6 +28,7 @@ import { } from '../../constants'; import { getAgentActionByIds } from './actions'; import { forceUnenrollAgent } from './unenroll'; +import { ackAgentUpgraded } from './upgrade'; const ALLOWED_ACKNOWLEDGEMENT_TYPE: string[] = ['ACTION_RESULT']; @@ -80,6 +81,11 @@ export async function acknowledgeAgentActions( await forceUnenrollAgent(soClient, agent.id); } + const upgradeAction = actions.find((action) => action.type === 'UPGRADE'); + if (upgradeAction) { + await ackAgentUpgraded(soClient, upgradeAction); + } + const configChangeAction = getLatestConfigChangePolicyActionIfUpdated(agent, actions); await soClient.bulkUpdate([ diff --git a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts index 1d4db44edf88a..f018eea61e4f3 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts @@ -225,7 +225,11 @@ export async function getAgentPolicyActionByIds( ); } -export async function getNewActionsSince(soClient: SavedObjectsClientContract, timestamp: string) { +export async function getNewActionsSince( + soClient: SavedObjectsClientContract, + timestamp: string, + decryptData: boolean = true +) { const filter = nodeTypes.function.buildNode('and', [ nodeTypes.function.buildNode( 'not', @@ -243,14 +247,33 @@ export async function getNewActionsSince(soClient: SavedObjectsClientContract, t } ), ]); - const res = await soClient.find({ - type: AGENT_ACTION_SAVED_OBJECT_TYPE, - filter, - }); - return res.saved_objects + const actions = ( + await soClient.find({ + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + filter, + }) + ).saved_objects .filter(isAgentActionSavedObject) .map((so) => savedObjectToAgentAction(so)); + + if (!decryptData) { + return actions; + } + + return await Promise.all( + actions.map(async (action) => { + // Get decrypted actions + return savedObjectToAgentAction( + await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser( + AGENT_ACTION_SAVED_OBJECT_TYPE, + action.id + ) + ); + }) + ); } export async function getLatestConfigChangeAction( diff --git a/x-pack/plugins/ingest_manager/server/services/agents/index.ts b/x-pack/plugins/ingest_manager/server/services/agents/index.ts index 400c099af4e93..c878b666bde88 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/index.ts @@ -9,6 +9,7 @@ export * from './events'; export * from './checkin'; export * from './enroll'; export * from './unenroll'; +export * from './upgrade'; export * from './status'; export * from './crud'; export * from './update'; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/upgrade.ts b/x-pack/plugins/ingest_manager/server/services/agents/upgrade.ts new file mode 100644 index 0000000000000..cee3bc69f25db --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agents/upgrade.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'src/core/server'; +import { AgentSOAttributes, AgentAction, AgentActionSOAttributes } from '../../types'; +import { AGENT_ACTION_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { createAgentAction } from './actions'; + +export async function sendUpgradeAgentAction({ + soClient, + agentId, + version, + sourceUri, +}: { + soClient: SavedObjectsClientContract; + agentId: string; + version: string; + sourceUri: string; +}) { + const now = new Date().toISOString(); + const data = { + version, + source_uri: sourceUri, + }; + await createAgentAction(soClient, { + agent_id: agentId, + created_at: now, + data, + ack_data: data, + type: 'UPGRADE', + }); + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + upgraded_at: undefined, + upgrade_started_at: now, + }); +} + +export async function ackAgentUpgraded( + soClient: SavedObjectsClientContract, + agentAction: AgentAction +) { + const { + attributes: { ack_data: ackData }, + } = await soClient.get(AGENT_ACTION_SAVED_OBJECT_TYPE, agentAction.id); + if (!ackData) throw new Error('data missing from UPGRADE action'); + const { version } = JSON.parse(ackData); + if (!version) throw new Error('version missing from UPGRADE action'); + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentAction.agent_id, { + upgraded_at: new Date().toISOString(), + local_metadata: { + elastic: { + agent: { + version, + }, + }, + }, + }); +} diff --git a/x-pack/plugins/ingest_manager/server/types/models/agent.ts b/x-pack/plugins/ingest_manager/server/types/models/agent.ts index b249705fe6c2f..15004e60a6fa4 100644 --- a/x-pack/plugins/ingest_manager/server/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/server/types/models/agent.ts @@ -62,7 +62,12 @@ export const AgentEventSchema = schema.object({ }); export const NewAgentActionSchema = schema.object({ - type: schema.oneOf([schema.literal('CONFIG_CHANGE'), schema.literal('UNENROLL')]), + type: schema.oneOf([ + schema.literal('CONFIG_CHANGE'), + schema.literal('UNENROLL'), + schema.literal('UPGRADE'), + ]), data: schema.maybe(schema.any()), + ack_data: schema.maybe(schema.any()), sent_at: schema.maybe(schema.string()), }); diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts index 4aefa56e0ca0a..3866ef095563e 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts @@ -172,6 +172,16 @@ export const PostAgentUnenrollRequestSchema = { ), }; +export const PostAgentUpgradeRequestSchema = { + params: schema.object({ + agentId: schema.string(), + }), + body: schema.object({ + source_uri: schema.string(), + version: schema.string(), + }), +}; + export const PostBulkAgentUnenrollRequestSchema = { body: schema.object({ agents: schema.oneOf([schema.arrayOf(schema.string()), schema.string()]), diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/acks.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/acks.ts index 1613ca9d11ee6..360b91203dfc8 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/acks.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/acks.ts @@ -13,6 +13,7 @@ export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const esArchiver = getService('esArchiver'); const esClient = getService('es'); + const kibanaServer = getService('kibanaServer'); const supertest = getSupertestWithoutAuth(providerContext); let apiKey: { id: string; api_key: string }; @@ -205,5 +206,50 @@ export default function (providerContext: FtrProviderContext) { 'ACTION not allowed for acknowledgment only ACTION_RESULT' ); }); + + it('ack upgrade should update fleet-agent SO', async () => { + const { body: actionRes } = await supertest + .post(`/api/ingest_manager/fleet/agents/agent1/actions`) + .set('kbn-xsrf', 'xx') + .set( + 'Authorization', + `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}` + ) + .send({ + action: { + type: 'UPGRADE', + ack_data: { version: '8.0.0', source_uri: 'http://localhost:8000' }, + }, + }) + .expect(200); + const actionId = actionRes.item.id; + await supertest + .post(`/api/ingest_manager/fleet/agents/agent1/acks`) + .set('kbn-xsrf', 'xx') + .set( + 'Authorization', + `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}` + ) + .send({ + events: [ + { + type: 'ACTION_RESULT', + subtype: 'ACKNOWLEDGED', + timestamp: '2020-09-21T13:25:29.02838-04:00', + action_id: actionId, + agent_id: 'agent1', + message: + "Action '70d97288-ffd9-4549-8c49-2423a844f67f' of type 'UPGRADE' acknowledged.", + }, + ], + }) + .expect(200); + + const res = await kibanaServer.savedObjects.get({ + type: 'fleet-agents', + id: 'agent1', + }); + expect(res.attributes.upgraded_at).to.be.ok(); + }); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/upgrade.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/upgrade.ts new file mode 100644 index 0000000000000..a783f806c03ee --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/upgrade.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect/expect.js'; +import { FtrProviderContext } from '../../../../api_integration/ftr_provider_context'; +import { setupIngest } from './services'; +import { skipIfNoDockerRegistry } from '../../../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + + describe('fleet upgrade agent', () => { + skipIfNoDockerRegistry(providerContext); + setupIngest(providerContext); + before(async () => { + await esArchiver.loadIfNeeded('fleet/agents'); + }); + after(async () => { + await esArchiver.unload('fleet/agents'); + }); + + it('should respond 200 to upgrade agent and update the agent SO', async () => { + const kibanaVersionAccessor = kibanaServer.version; + const kibanaVersion = await kibanaVersionAccessor.get(); + await supertest + .post(`/api/ingest_manager/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: kibanaVersion, + source_uri: 'http://path/to/download', + }) + .expect(200); + const res = await kibanaServer.savedObjects.get({ + type: 'fleet-agents', + id: 'agent1', + }); + expect(res.attributes.upgrade_started_at).to.be.ok(); + }); + + it('should respond 400 if trying to upgrade to a version that does not match installed kibana version', async () => { + await supertest + .post(`/api/ingest_manager/fleet/agents/agent1/upgrade`) + .set('kbn-xsrf', 'xxx') + .send({ + version: '8.0.1', + source_uri: 'http://path/to/download', + }) + .expect(400); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/index.js b/x-pack/test/ingest_manager_api_integration/apis/fleet/index.js index 96b9ffd1b04c0..9eb51bee3c7dd 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/index.js @@ -18,6 +18,7 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./enrollment_api_keys/crud')); loadTestFile(require.resolve('./install')); loadTestFile(require.resolve('./agents/actions')); + loadTestFile(require.resolve('./agents/upgrade')); loadTestFile(require.resolve('./agents/reassign')); }); } From 32abbff6858122fc86780e11af53e2e6af18370b Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Wed, 23 Sep 2020 17:40:52 -0400 Subject: [PATCH 69/92] [Time to Visualize] Lens By Value With AttributeService (#77561) Used the attribute service to make lens work properly with by value embeddables. --- .../actions/library_notification_action.tsx | 1 + .../application/dashboard_app_controller.tsx | 55 +- ...ce_mock.tsx => attribute_service.mock.tsx} | 0 .../attribute_service.test.ts | 2 +- .../attribute_service/attribute_service.tsx | 17 +- .../public/attribute_service/index.ts | 20 + src/plugins/dashboard/public/index.ts | 4 +- src/plugins/dashboard/public/mocks.tsx | 1 + src/plugins/dashboard/public/plugin.tsx | 2 +- .../lib/actions/edit_panel_action.test.tsx | 42 +- .../public/lib/actions/edit_panel_action.ts | 12 +- .../public/lib/containers/container.ts | 2 +- .../embeddable_state_transfer.test.ts | 27 +- .../public/lib/state_transfer/types.ts | 30 +- .../saved_object_save_modal_origin.tsx | 14 +- .../public/wizard/new_vis_modal.tsx | 4 +- .../public/wizard/show_new_vis.tsx | 1 + .../application/utils/get_top_nav_config.tsx | 16 +- x-pack/plugins/lens/common/constants.ts | 5 +- .../lens/public/app_plugin/app.test.tsx | 992 +++++++++--------- x-pack/plugins/lens/public/app_plugin/app.tsx | 866 +++++++-------- .../lens/public/app_plugin/lens_top_nav.tsx | 73 ++ .../lens/public/app_plugin/mounter.tsx | 146 ++- .../plugins/lens/public/app_plugin/types.ts | 102 ++ .../editor_frame_service/editor_frame/save.ts | 2 +- .../editor_frame/state_management.test.ts | 2 +- .../editor_frame/state_management.ts | 2 +- .../embeddable/embeddable.test.tsx | 240 +++-- .../embeddable/embeddable.tsx | 196 ++-- .../embeddable/embeddable_factory.ts | 74 +- .../editor_frame_service/service.test.tsx | 6 +- .../public/editor_frame_service/service.tsx | 12 +- .../lens/public/lens_attribute_service.ts | 52 + .../persistence/saved_object_store.test.ts | 6 +- .../public/persistence/saved_object_store.ts | 30 +- x-pack/plugins/lens/public/plugin.ts | 33 +- .../routes/maps_app/top_nav_config.tsx | 12 +- 37 files changed, 1783 insertions(+), 1318 deletions(-) rename src/plugins/dashboard/public/attribute_service/{attribute_service_mock.tsx => attribute_service.mock.tsx} (100%) create mode 100644 src/plugins/dashboard/public/attribute_service/index.ts create mode 100644 x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx create mode 100644 x-pack/plugins/lens/public/app_plugin/types.ts create mode 100644 x-pack/plugins/lens/public/lens_attribute_service.ts diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx index 974b55275ccc1..bff0236c802f1 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx @@ -79,6 +79,7 @@ export class LibraryNotificationAction implements ActionByType { return ( + embeddable.getRoot().isContainer && embeddable.getInput()?.viewMode !== ViewMode.VIEW && isReferenceOrValueEmbeddable(embeddable) && embeddable.inputIsRefType(embeddable.getInput()) diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index dd5eb1ee5ccaa..e5b467a418177 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -66,7 +66,6 @@ import { ViewMode, ContainerOutput, EmbeddableInput, - SavedObjectEmbeddableInput, } from '../../../embeddable/public'; import { NavAction, SavedDashboardPanel } from '../types'; @@ -178,7 +177,7 @@ export class DashboardAppController { chrome.docTitle.change(dash.title); } - const incomingEmbeddable = embeddable + let incomingEmbeddable = embeddable .getStateTransfer(scopedHistory()) .getIncomingEmbeddablePackage(); @@ -344,6 +343,22 @@ export class DashboardAppController { dashboardStateManager.getPanels().forEach((panel: SavedDashboardPanel) => { embeddablesMap[panel.panelIndex] = convertSavedDashboardPanelToPanelState(panel); }); + + // If the incoming embeddable state's id already exists in the embeddables map, replace the input, retaining the existing gridData for that panel. + if (incomingEmbeddable?.embeddableId && embeddablesMap[incomingEmbeddable.embeddableId]) { + const originalPanelState = embeddablesMap[incomingEmbeddable.embeddableId]; + embeddablesMap[incomingEmbeddable.embeddableId] = { + gridData: originalPanelState.gridData, + type: incomingEmbeddable.type, + explicitInput: { + ...originalPanelState.explicitInput, + ...incomingEmbeddable.input, + id: incomingEmbeddable.embeddableId, + }, + }; + incomingEmbeddable = undefined; + } + let expandedPanelId; if (dashboardContainer && !isErrorEmbeddable(dashboardContainer)) { expandedPanelId = dashboardContainer.getInput().expandedPanelId; @@ -482,32 +497,16 @@ export class DashboardAppController { refreshDashboardContainer(); }); - if (incomingEmbeddable) { - if ('id' in incomingEmbeddable) { - container.addOrUpdateEmbeddable( - incomingEmbeddable.type, - { - savedObjectId: incomingEmbeddable.id, - } - ); - } else if ('input' in incomingEmbeddable) { - const input = incomingEmbeddable.input; - // @ts-expect-error - delete input.id; - const explicitInput = { - savedVis: input, - }; - const embeddableId = - 'embeddableId' in incomingEmbeddable - ? incomingEmbeddable.embeddableId - : undefined; - container.addOrUpdateEmbeddable( - incomingEmbeddable.type, - // This ugly solution is temporary - https://github.com/elastic/kibana/pull/70272 fixes this whole section - (explicitInput as unknown) as EmbeddableInput, - embeddableId - ); - } + // If the incomingEmbeddable does not yet exist in the panels listing, create a new panel using the container's addEmbeddable method. + if ( + incomingEmbeddable && + (!incomingEmbeddable.embeddableId || + !container.getInput().panels[incomingEmbeddable.embeddableId]) + ) { + container.addNewEmbeddable( + incomingEmbeddable.type, + incomingEmbeddable.input + ); } } diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service_mock.tsx b/src/plugins/dashboard/public/attribute_service/attribute_service.mock.tsx similarity index 100% rename from src/plugins/dashboard/public/attribute_service/attribute_service_mock.tsx rename to src/plugins/dashboard/public/attribute_service/attribute_service.mock.tsx diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.test.ts b/src/plugins/dashboard/public/attribute_service/attribute_service.test.ts index 06f380ca3862b..ae8f034aec687 100644 --- a/src/plugins/dashboard/public/attribute_service/attribute_service.test.ts +++ b/src/plugins/dashboard/public/attribute_service/attribute_service.test.ts @@ -18,7 +18,7 @@ */ import { ATTRIBUTE_SERVICE_KEY } from './attribute_service'; -import { mockAttributeService } from './attribute_service_mock'; +import { mockAttributeService } from './attribute_service.mock'; import { coreMock } from '../../../../core/public/mocks'; interface TestAttributes { diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx index 84df05154fb63..7499a6fced72a 100644 --- a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx +++ b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx @@ -156,12 +156,8 @@ export class AttributeService< }; public getExplicitInputFromEmbeddable(embeddable: IEmbeddable): ValType | RefType { - return embeddable.getRoot() && - (embeddable.getRoot() as Container).getInput().panels[embeddable.id].explicitInput - ? ((embeddable.getRoot() as Container).getInput().panels[embeddable.id].explicitInput as - | ValType - | RefType) - : (embeddable.getInput() as ValType | RefType); + return ((embeddable.getRoot() as Container).getInput()?.panels?.[embeddable.id] + ?.explicitInput ?? embeddable.getInput()) as ValType | RefType; } getInputAsValueType = async (input: ValType | RefType): Promise => { @@ -204,7 +200,14 @@ export class AttributeService< const newAttributes = { ...input[ATTRIBUTE_SERVICE_KEY] }; newAttributes.title = props.newTitle; const wrappedInput = (await this.wrapAttributes(newAttributes, true)) as RefType; - resolve(wrappedInput); + + // Remove unneeded attributes from the original input. + delete (input as { [ATTRIBUTE_SERVICE_KEY]?: SavedObjectAttributes })[ + ATTRIBUTE_SERVICE_KEY + ]; + + // Combine input and wrapped input to preserve any passed in explicit Input. + resolve({ ...input, ...wrappedInput }); return { id: wrappedInput.savedObjectId }; } catch (error) { reject(error); diff --git a/src/plugins/dashboard/public/attribute_service/index.ts b/src/plugins/dashboard/public/attribute_service/index.ts new file mode 100644 index 0000000000000..84d4c8a13c31e --- /dev/null +++ b/src/plugins/dashboard/public/attribute_service/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { AttributeService, ATTRIBUTE_SERVICE_KEY } from './attribute_service'; diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index e22d1f038a456..315afd61c7c44 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -31,7 +31,7 @@ export { } from './application'; export { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; -export { DashboardStart, DashboardUrlGenerator } from './plugin'; +export { DashboardStart, DashboardUrlGenerator, DashboardFeatureFlagConfig } from './plugin'; export { DASHBOARD_APP_URL_GENERATOR, createDashboardUrlGenerator, @@ -40,7 +40,7 @@ export { export { addEmbeddableToDashboardUrl } from './url_utils/url_helper'; export { SavedObjectDashboard } from './saved_dashboards'; export { SavedDashboardPanel } from './types'; -export { AttributeService, ATTRIBUTE_SERVICE_KEY } from './attribute_service/attribute_service'; +export { AttributeService, ATTRIBUTE_SERVICE_KEY } from './attribute_service'; export function plugin(initializerContext: PluginInitializerContext) { return new DashboardPlugin(initializerContext); diff --git a/src/plugins/dashboard/public/mocks.tsx b/src/plugins/dashboard/public/mocks.tsx index ba30d72594f2a..07f29eca53042 100644 --- a/src/plugins/dashboard/public/mocks.tsx +++ b/src/plugins/dashboard/public/mocks.tsx @@ -20,6 +20,7 @@ import { DashboardStart } from './plugin'; export type Start = jest.Mocked; +export { mockAttributeService } from './attribute_service/attribute_service.mock'; const createStartContract = (): DashboardStart => { // @ts-ignore diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 5a45229a58a7d..eadb3cd207e4d 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -117,7 +117,7 @@ declare module '../../share/public' { export type DashboardUrlGenerator = UrlGeneratorContract; -interface DashboardFeatureFlagConfig { +export interface DashboardFeatureFlagConfig { allowByValueEmbeddables: boolean; } diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx index 8c3d7ab9c30d0..ba24913c6d1c0 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx @@ -18,7 +18,7 @@ */ import { EditPanelAction } from './edit_panel_action'; -import { Embeddable, EmbeddableInput } from '../embeddables'; +import { Embeddable, EmbeddableInput, SavedObjectEmbeddableInput } from '../embeddables'; import { ViewMode } from '../types'; import { ContactCardEmbeddable } from '../test_samples'; import { embeddablePluginMock } from '../../mocks'; @@ -53,20 +53,50 @@ test('is compatible when edit url is available, in edit mode and editable', asyn ).toBe(true); }); -test('redirects to app using state transfer', async () => { +test('redirects to app using state transfer with by value mode', async () => { applicationMock.currentAppId$ = of('superCoolCurrentApp'); const action = new EditPanelAction(getFactory, applicationMock, stateTransferMock); - const input = { id: '123', viewMode: ViewMode.EDIT }; - const embeddable = new EditableEmbeddable(input, true); + const embeddable = new EditableEmbeddable( + ({ + id: '123', + viewMode: ViewMode.EDIT, + coolInput1: 1, + coolInput2: 2, + } as unknown) as EmbeddableInput, + true + ); + embeddable.getOutput = jest.fn(() => ({ editApp: 'ultraVisualize', editPath: '/123' })); + await action.execute({ embeddable }); + expect(stateTransferMock.navigateToEditor).toHaveBeenCalledWith('ultraVisualize', { + path: '/123', + state: { + originatingApp: 'superCoolCurrentApp', + embeddableId: '123', + valueInput: { + id: '123', + viewMode: ViewMode.EDIT, + coolInput1: 1, + coolInput2: 2, + }, + }, + }); +}); + +test('redirects to app using state transfer without by value mode', async () => { + applicationMock.currentAppId$ = of('superCoolCurrentApp'); + const action = new EditPanelAction(getFactory, applicationMock, stateTransferMock); + const embeddable = new EditableEmbeddable( + { id: '123', viewMode: ViewMode.EDIT, savedObjectId: '1234' } as SavedObjectEmbeddableInput, + true + ); embeddable.getOutput = jest.fn(() => ({ editApp: 'ultraVisualize', editPath: '/123' })); await action.execute({ embeddable }); expect(stateTransferMock.navigateToEditor).toHaveBeenCalledWith('ultraVisualize', { path: '/123', state: { originatingApp: 'superCoolCurrentApp', - byValueMode: true, embeddableId: '123', - valueInput: input, + valueInput: undefined, }, }); }); diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts index 8d12ddd0299e7..cbc28713197ba 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts @@ -29,6 +29,8 @@ import { EmbeddableEditorState, EmbeddableStateTransfer, SavedObjectEmbeddableInput, + EmbeddableInput, + Container, } from '../..'; export const ACTION_EDIT_PANEL = 'editPanel'; @@ -118,8 +120,7 @@ export class EditPanelAction implements Action { const byValueMode = !(embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId; const state: EmbeddableEditorState = { originatingApp: this.currentAppId, - byValueMode, - valueInput: byValueMode ? embeddable.getInput() : undefined, + valueInput: byValueMode ? this.getExplicitInput({ embeddable }) : undefined, embeddableId: embeddable.id, }; return { app, path, state }; @@ -132,4 +133,11 @@ export class EditPanelAction implements Action { const editUrl = embeddable ? embeddable.getOutput().editUrl : undefined; return editUrl ? editUrl : ''; } + + private getExplicitInput({ embeddable }: ActionContext): EmbeddableInput { + return ( + (embeddable.getRoot() as Container)?.getInput()?.panels?.[embeddable.id]?.explicitInput ?? + embeddable.getInput() + ); + } } diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts index 38975cc220bc2..9f701f021162a 100644 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ b/src/plugins/embeddable/public/lib/containers/container.ts @@ -199,8 +199,8 @@ export abstract class Container< return { type: factory.type, explicitInput: { - id: embeddableId, ...explicitInput, + id: embeddableId, } as TEmbeddableInput, }; } diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts index ef79b18acd4f5..4155cb4d3b60c 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts @@ -85,10 +85,10 @@ describe('embeddable state transfer', () => { it('can send an outgoing embeddable package state', async () => { await stateTransfer.navigateToWithEmbeddablePackage(destinationApp, { - state: { type: 'coolestType', id: '150' }, + state: { type: 'coolestType', input: { savedObjectId: '150' } }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { - state: { type: 'coolestType', id: '150' }, + state: { type: 'coolestType', input: { savedObjectId: '150' } }, }); }); @@ -96,12 +96,16 @@ describe('embeddable state transfer', () => { const historyMock = mockHistoryState({ kibanaIsNowForSports: 'extremeSportsKibana' }); stateTransfer = new EmbeddableStateTransfer(application.navigateToApp, historyMock); await stateTransfer.navigateToWithEmbeddablePackage(destinationApp, { - state: { type: 'coolestType', id: '150' }, + state: { type: 'coolestType', input: { savedObjectId: '150' } }, appendToExistingState: true, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, - state: { kibanaIsNowForSports: 'extremeSportsKibana', type: 'coolestType', id: '150' }, + state: { + kibanaIsNowForSports: 'extremeSportsKibana', + type: 'coolestType', + input: { savedObjectId: '150' }, + }, }); }); @@ -120,10 +124,13 @@ describe('embeddable state transfer', () => { }); it('can fetch an incoming embeddable package state', async () => { - const historyMock = mockHistoryState({ type: 'skisEmbeddable', id: '123' }); + const historyMock = mockHistoryState({ + type: 'skisEmbeddable', + input: { savedObjectId: '123' }, + }); stateTransfer = new EmbeddableStateTransfer(application.navigateToApp, historyMock); const fetchedState = stateTransfer.getIncomingEmbeddablePackage(); - expect(fetchedState).toEqual({ type: 'skisEmbeddable', id: '123' }); + expect(fetchedState).toEqual({ type: 'skisEmbeddable', input: { savedObjectId: '123' } }); }); it('returns undefined when embeddable package is not in the right shape', async () => { @@ -136,12 +143,12 @@ describe('embeddable state transfer', () => { it('removes all keys in the keysToRemoveAfterFetch array', async () => { const historyMock = mockHistoryState({ type: 'skisEmbeddable', - id: '123', + input: { savedObjectId: '123' }, test1: 'test1', test2: 'test2', }); stateTransfer = new EmbeddableStateTransfer(application.navigateToApp, historyMock); - stateTransfer.getIncomingEmbeddablePackage({ keysToRemoveAfterFetch: ['type', 'id'] }); + stateTransfer.getIncomingEmbeddablePackage({ keysToRemoveAfterFetch: ['type', 'input'] }); expect(historyMock.replace).toHaveBeenCalledWith( expect.objectContaining({ state: { test1: 'test1', test2: 'test2' } }) ); @@ -150,7 +157,7 @@ describe('embeddable state transfer', () => { it('leaves state as is when no keysToRemove are supplied', async () => { const historyMock = mockHistoryState({ type: 'skisEmbeddable', - id: '123', + input: { savedObjectId: '123' }, test1: 'test1', test2: 'test2', }); @@ -158,7 +165,7 @@ describe('embeddable state transfer', () => { stateTransfer.getIncomingEmbeddablePackage(); expect(historyMock.location.state).toEqual({ type: 'skisEmbeddable', - id: '123', + input: { savedObjectId: '123' }, test1: 'test1', test2: 'test2', }); diff --git a/src/plugins/embeddable/public/lib/state_transfer/types.ts b/src/plugins/embeddable/public/lib/state_transfer/types.ts index 3f3456d914728..d8b4f4801bba3 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/types.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/types.ts @@ -17,17 +17,17 @@ * under the License. */ -import { EmbeddableInput } from '..'; +import { Optional } from '@kbn/utility-types'; +import { EmbeddableInput, SavedObjectEmbeddableInput } from '..'; /** - * Represents a state package that contains the last active app id. + * A state package that contains information an editor will need to create or edit an embeddable then redirect back. * @public */ export interface EmbeddableEditorState { originatingApp: string; - byValueMode?: boolean; - valueInput?: EmbeddableInput; embeddableId?: string; + valueInput?: EmbeddableInput; } export function isEmbeddableEditorState(state: unknown): state is EmbeddableEditorState { @@ -35,32 +35,18 @@ export function isEmbeddableEditorState(state: unknown): state is EmbeddableEdit } /** - * Represents a state package that contains all fields necessary to create an embeddable by reference in a container. + * A state package that contains all fields necessary to create or update an embeddable by reference or by value in a container. * @public */ -export interface EmbeddablePackageByReferenceState { +export interface EmbeddablePackageState { type: string; - id: string; -} - -/** - * Represents a state package that contains all fields necessary to create an embeddable by value in a container. - * @public - */ -export interface EmbeddablePackageByValueState { - type: string; - input: EmbeddableInput; + input: Optional | Optional; embeddableId?: string; } -export type EmbeddablePackageState = - | EmbeddablePackageByReferenceState - | EmbeddablePackageByValueState; - export function isEmbeddablePackageState(state: unknown): state is EmbeddablePackageState { return ( - (ensureFieldOfTypeExists('type', state, 'string') && - ensureFieldOfTypeExists('id', state, 'string')) || + ensureFieldOfTypeExists('type', state, 'string') && ensureFieldOfTypeExists('input', state, 'object') ); } diff --git a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal_origin.tsx b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal_origin.tsx index ce08151d37c2c..dfc0c4049774d 100644 --- a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal_origin.tsx +++ b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal_origin.tsx @@ -33,6 +33,8 @@ interface SaveModalDocumentInfo { interface OriginSaveModalProps { originatingApp?: string; getAppNameFromId?: (appId: string) => string | undefined; + originatingAppName?: string; + returnToOriginSwitchLabel?: string; documentInfo: SaveModalDocumentInfo; objectType: string; onClose: () => void; @@ -73,11 +75,13 @@ export function SavedObjectSaveModalOrigin(props: OriginSaveModalProps) { setReturnToOriginMode(event.target.checked); }} label={ - + props.returnToOriginSwitchLabel ?? ( + + ) } /> diff --git a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx index 1d01900ceffc2..a9bf6bd171f15 100644 --- a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx +++ b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx @@ -174,7 +174,9 @@ class NewVisModal extends React.Component void; originatingApp?: string; outsideVisualizeApp?: boolean; + createByValue?: boolean; } /** diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index 65c9a5410d226..12720f3f22e7c 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -21,8 +21,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { TopNavMenuData } from 'src/plugins/navigation/public'; -import uuid from 'uuid'; -import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../visualizations/public'; +import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeInput } from '../../../../visualizations/public'; import { showSaveModal, SavedObjectSaveModalOrigin, @@ -122,7 +121,7 @@ export const getTopNavConfig = ( if (newlyCreated && stateTransfer) { stateTransfer.navigateToWithEmbeddablePackage(originatingApp, { - state: { id, type: VISUALIZE_EMBEDDABLE_TYPE }, + state: { type: VISUALIZE_EMBEDDABLE_TYPE, input: { savedObjectId: id } }, }); } else { application.navigateToApp(originatingApp); @@ -167,15 +166,11 @@ export const getTopNavConfig = ( } const state = { input: { - ...vis.serialize(), - id: embeddableId ? embeddableId : uuid.v4(), - }, + savedVis: vis.serialize(), + } as VisualizeInput, + embeddableId, type: VISUALIZE_EMBEDDABLE_TYPE, - embeddableId: '', }; - if (embeddableId) { - state.embeddableId = embeddableId; - } stateTransfer.navigateToWithEmbeddablePackage(originatingApp, { state }); }; @@ -283,6 +278,7 @@ export const getTopNavConfig = ( } return response; }; + const saveModal = ( { - let frame: jest.Mocked; let core: ReturnType; - let instance: ReactWrapper; - - function makeDefaultArgs(): jest.Mocked<{ - editorFrame: EditorFrameInstance; - data: typeof dataStartMock; - navigation: typeof navigationStartMock; - core: typeof core; - storage: Storage; - docId?: string; - docStorage: SavedObjectStore; - redirectTo: (id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => void; - originatingApp: string | undefined; - onAppLeave: AppMountParameters['onAppLeave']; - setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; - history: History; - getAppNameFromId?: (appId: string) => string | undefined; - }> { - return ({ - navigation: navigationStartMock, + let defaultDoc: Document; + let defaultSavedObjectId: string; + + const navMenuItems = { + expectedSaveButton: { emphasize: true, testId: 'lnsApp_saveButton' }, + expectedSaveAsButton: { emphasize: false, testId: 'lnsApp_saveButton' }, + expectedSaveAndReturnButton: { emphasize: true, testId: 'lnsApp_saveAndReturnButton' }, + }; + + function makeAttributeService(): LensAttributeService { + const attributeServiceMock = mockAttributeService< + LensSavedObjectAttributes, + LensByValueInput, + LensByReferenceInput + >( + DOC_TYPE, + { + customSaveMethod: jest.fn(), + customUnwrapMethod: jest.fn(), + }, + core + ); + attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc); + attributeServiceMock.wrapAttributes = jest + .fn() + .mockResolvedValue({ savedObjectId: defaultSavedObjectId }); + return attributeServiceMock; + } + + function makeDefaultProps(): jest.Mocked { + return { editorFrame: createMockFrame(), - core: { - ...core, - application: { - ...core.application, - capabilities: { - ...core.application.capabilities, - visualize: { save: true, saveQuery: true, show: true }, - }, + history: createMemoryHistory(), + redirectTo: jest.fn(), + redirectToOrigin: jest.fn(), + onAppLeave: jest.fn(), + setHeaderActionMenu: jest.fn(), + }; + } + + function makeDefaultServices(): jest.Mocked { + return { + http: core.http, + chrome: core.chrome, + overlays: core.overlays, + uiSettings: core.uiSettings, + navigation: navigationStartMock, + notifications: core.notifications, + attributeService: makeAttributeService(), + savedObjectsClient: core.savedObjects.client, + dashboardFeatureFlag: { allowByValueEmbeddables: false }, + getOriginatingAppName: jest.fn(() => 'defaultOriginatingApp'), + application: { + ...core.application, + capabilities: { + ...core.application.capabilities, + visualize: { save: true, saveQuery: true, show: true }, }, + getUrlForApp: jest.fn((appId: string) => `/testbasepath/app/${appId}#/`), }, - data: { + data: ({ query: { filterManager: createMockFilterManager(), timefilter: { @@ -166,38 +202,52 @@ describe('Lens App', () => { return new Promise((resolve) => resolve({ id })); }), }, - }, + } as unknown) as DataPublicPluginStart, storage: { get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), }, - docStorage: { - load: jest.fn(), - save: jest.fn(), - }, - redirectTo: jest.fn((id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => {}), - onAppLeave: jest.fn(), - setHeaderActionMenu: jest.fn(), - history: createMemoryHistory(), - } as unknown) as jest.Mocked<{ - navigation: typeof navigationStartMock; - editorFrame: EditorFrameInstance; - data: typeof dataStartMock; - core: typeof core; - storage: Storage; - docId?: string; - docStorage: SavedObjectStore; - redirectTo: (id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => void; - originatingApp: string | undefined; - onAppLeave: AppMountParameters['onAppLeave']; - setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; - history: History; - getAppNameFromId?: (appId: string) => string | undefined; - }>; + }; + } + + function mountWith({ + props: incomingProps, + services: incomingServices, + }: { + props?: jest.Mocked; + services?: jest.Mocked; + }) { + const props = incomingProps ?? makeDefaultProps(); + const services = incomingServices ?? makeDefaultServices(); + const wrappingComponent: React.FC<{ + children: React.ReactNode; + }> = ({ children }) => { + return ( + + {children} + + ); + }; + const frame = props.editorFrame as ReturnType; + const component = mount(, { wrappingComponent }); + return { component, frame, props, services }; } beforeEach(() => { - frame = createMockFrame(); core = coreMock.createStart({ basePath: '/testbasepath' }); + defaultSavedObjectId = '1234'; + defaultDoc = ({ + savedObjectId: defaultSavedObjectId, + title: 'An extremely cool default document!', + expression: 'definitely a valid expression', + state: { + query: 'kuery', + filters: [{ query: { match_phrase: { src: 'test' } } }], + }, + references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], + } as unknown) as Document; core.uiSettings.get.mockImplementation( jest.fn((type) => { @@ -215,10 +265,7 @@ describe('Lens App', () => { }); it('renders the editor frame', () => { - const args = makeDefaultArgs(); - args.editorFrame = frame; - - mount(); + const { frame } = mountWith({}); expect(frame.mount.mock.calls).toMatchInlineSnapshot(` Array [ @@ -248,23 +295,22 @@ describe('Lens App', () => { }); it('clears app filters on load', () => { - const defaultArgs = makeDefaultArgs(); - mount(); - - expect(defaultArgs.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([]); + const { services } = mountWith({}); + expect(services.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([]); }); it('passes global filters to frame', async () => { - const args = makeDefaultArgs(); - args.editorFrame = frame; + const services = makeDefaultServices(); const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; const pinnedFilter = esFilters.buildExistsFilter(pinnedField, indexPattern); - args.data.query.filterManager.getFilters = jest.fn().mockImplementation(() => { + services.data.query.filterManager.getFilters = jest.fn().mockImplementation(() => { return [pinnedFilter]; }); - const component = mount(); + const { component, frame } = mountWith({ services }); + component.update(); + expect(frame.mount).toHaveBeenCalledWith( expect.any(Element), expect.objectContaining({ @@ -275,103 +321,81 @@ describe('Lens App', () => { ); }); - it('sets breadcrumbs when the document title changes', async () => { - const defaultArgs = makeDefaultArgs(); - instance = mount(); - - expect(core.chrome.setBreadcrumbs).toHaveBeenCalledWith([ - { text: 'Visualize', href: '/testbasepath/app/visualize#/', onClick: expect.anything() }, - { text: 'Create' }, - ]); + it('displays errors from the frame in a toast', () => { + const { component, frame, services } = mountWith({}); + const onError = frame.mount.mock.calls[0][1].onError; + onError({ message: 'error' }); + component.update(); + expect(services.notifications.toasts.addDanger).toHaveBeenCalled(); + }); - (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ - id: '1234', + describe('breadcrumbs', () => { + const breadcrumbDocSavedObjectId = defaultSavedObjectId; + const breadcrumbDoc = ({ + savedObjectId: breadcrumbDocSavedObjectId, title: 'Daaaaaaadaumching!', state: { query: 'fake query', filters: [], }, references: [], - }); - await act(async () => { - instance.setProps({ docId: '1234' }); - }); + } as unknown) as Document; - expect(defaultArgs.core.chrome.setBreadcrumbs).toHaveBeenCalledWith([ - { text: 'Visualize', href: '/testbasepath/app/visualize#/', onClick: expect.anything() }, - { text: 'Daaaaaaadaumching!' }, - ]); - }); + it('sets breadcrumbs when the document title changes', async () => { + const { component, services } = mountWith({}); - it('adds to the recently viewed list on load', async () => { - const defaultArgs = makeDefaultArgs(); - instance = mount(); + expect(services.chrome.setBreadcrumbs).toHaveBeenCalledWith([ + { text: 'Visualize', href: '/testbasepath/app/visualize#/', onClick: expect.anything() }, + { text: 'Create' }, + ]); - (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ - id: '1234', - title: 'Daaaaaaadaumching!', - state: { - query: 'fake query', - filters: [], - }, - references: [], - }); - await act(async () => { - instance.setProps({ docId: '1234' }); + services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(breadcrumbDoc); + await act(async () => { + component.setProps({ initialInput: { savedObjectId: breadcrumbDocSavedObjectId } }); + }); + + expect(services.chrome.setBreadcrumbs).toHaveBeenCalledWith([ + { text: 'Visualize', href: '/testbasepath/app/visualize#/', onClick: expect.anything() }, + { text: 'Daaaaaaadaumching!' }, + ]); }); - expect(defaultArgs.core.chrome.recentlyAccessed.add).toHaveBeenCalledWith( - '/app/lens#/edit/1234', - 'Daaaaaaadaumching!', - '1234' - ); - }); - it('sets originatingApp breadcrumb when the document title changes', async () => { - const defaultArgs = makeDefaultArgs(); - defaultArgs.originatingApp = 'ultraCoolDashboard'; - defaultArgs.getAppNameFromId = () => 'The Coolest Container Ever Made'; - instance = mount(); + it('sets originatingApp breadcrumb when the document title changes', async () => { + const props = makeDefaultProps(); + const services = makeDefaultServices(); + props.incomingState = { originatingApp: 'coolContainer' }; + services.getOriginatingAppName = jest.fn(() => 'The Coolest Container Ever Made'); + const { component } = mountWith({ props, services }); + + expect(services.chrome.setBreadcrumbs).toHaveBeenCalledWith([ + { text: 'The Coolest Container Ever Made', onClick: expect.anything() }, + { text: 'Visualize', href: '/testbasepath/app/visualize#/', onClick: expect.anything() }, + { text: 'Create' }, + ]); - expect(core.chrome.setBreadcrumbs).toHaveBeenCalledWith([ - { text: 'The Coolest Container Ever Made', onClick: expect.anything() }, - { text: 'Visualize', href: '/testbasepath/app/visualize#/', onClick: expect.anything() }, - { text: 'Create' }, - ]); + services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(breadcrumbDoc); + await act(async () => { + component.setProps({ initialInput: { savedObjectId: breadcrumbDocSavedObjectId } }); + }); - (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ - id: '1234', - title: 'Daaaaaaadaumching!', - state: { - query: 'fake query', - filters: [], - }, - references: [], - }); - await act(async () => { - instance.setProps({ docId: '1234' }); + expect(services.chrome.setBreadcrumbs).toHaveBeenCalledWith([ + { text: 'The Coolest Container Ever Made', onClick: expect.anything() }, + { text: 'Visualize', href: '/testbasepath/app/visualize#/', onClick: expect.anything() }, + { text: 'Daaaaaaadaumching!' }, + ]); }); - - expect(defaultArgs.core.chrome.setBreadcrumbs).toHaveBeenCalledWith([ - { text: 'The Coolest Container Ever Made', onClick: expect.anything() }, - { text: 'Visualize', href: '/testbasepath/app/visualize#/', onClick: expect.anything() }, - { text: 'Daaaaaaadaumching!' }, - ]); }); describe('persistence', () => { - it('does not load a document if there is no document id', () => { - const args = makeDefaultArgs(); - - mount(); - - expect(args.docStorage.load).not.toHaveBeenCalled(); + it('does not load a document if there is no initial input', () => { + const { services } = mountWith({}); + expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled(); }); - it('loads a document and uses query and filters if there is a document id', async () => { - const args = makeDefaultArgs(); - args.editorFrame = frame; - (args.docStorage.load as jest.Mock).mockResolvedValue({ - id: '1234', + it('loads a document and uses query and filters if initial input is provided', async () => { + const { component, frame, services } = mountWith({}); + services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + savedObjectId: defaultSavedObjectId, state: { query: 'fake query', filters: [{ query: { match_phrase: { src: 'test' } } }], @@ -379,15 +403,15 @@ describe('Lens App', () => { references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], }); - instance = mount(); - await act(async () => { - instance.setProps({ docId: '1234' }); + component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); }); - expect(args.docStorage.load).toHaveBeenCalledWith('1234'); - expect(args.data.indexPatterns.get).toHaveBeenCalledWith('1'); - expect(args.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([ + expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({ + savedObjectId: defaultSavedObjectId, + }); + expect(services.data.indexPatterns.get).toHaveBeenCalledWith('1'); + expect(services.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([ { query: { match_phrase: { src: 'test' } } }, ]); expect(TopNavMenu).toHaveBeenCalledWith( @@ -401,7 +425,7 @@ describe('Lens App', () => { expect.any(Element), expect.objectContaining({ doc: expect.objectContaining({ - id: '1234', + savedObjectId: defaultSavedObjectId, state: expect.objectContaining({ query: 'fake query', filters: [{ query: { match_phrase: { src: 'test' } } }], @@ -412,65 +436,59 @@ describe('Lens App', () => { }); it('does not load documents on sequential renders unless the id changes', async () => { - const args = makeDefaultArgs(); - args.editorFrame = frame; - (args.docStorage.load as jest.Mock).mockResolvedValue({ id: '1234' }); + const { services, component } = mountWith({}); - instance = mount(); await act(async () => { - instance.setProps({ docId: '1234' }); + component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); }); - await act(async () => { - instance.setProps({ docId: '1234' }); + component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); }); - - expect(args.docStorage.load).toHaveBeenCalledTimes(1); + expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1); await act(async () => { - instance.setProps({ docId: '9876' }); + component.setProps({ initialInput: { savedObjectId: '5678' } }); }); - expect(args.docStorage.load).toHaveBeenCalledTimes(2); + expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(2); }); it('handles document load errors', async () => { - const args = makeDefaultArgs(); - args.editorFrame = frame; - (args.docStorage.load as jest.Mock).mockRejectedValue('failed to load'); - - instance = mount(); + const services = makeDefaultServices(); + services.attributeService.unwrapAttributes = jest.fn().mockRejectedValue('failed to load'); + const { component, props } = mountWith({ services }); await act(async () => { - instance.setProps({ docId: '1234' }); + component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); }); - expect(args.docStorage.load).toHaveBeenCalledWith('1234'); - expect(args.core.notifications.toasts.addDanger).toHaveBeenCalled(); - expect(args.redirectTo).toHaveBeenCalled(); + expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({ + savedObjectId: defaultSavedObjectId, + }); + expect(services.notifications.toasts.addDanger).toHaveBeenCalled(); + expect(props.redirectTo).toHaveBeenCalled(); }); - describe('save button', () => { + it('adds to the recently accessed list on load', async () => { + const { component, services } = mountWith({}); + + await act(async () => { + component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); + }); + expect(services.chrome.recentlyAccessed.add).toHaveBeenCalledWith( + '/app/lens#/edit/1234', + 'An extremely cool default document!', + '1234' + ); + }); + + describe('save buttons', () => { interface SaveProps { newCopyOnSave: boolean; returnToOrigin?: boolean; newTitle: string; } - let defaultArgs: ReturnType; - - beforeEach(() => { - defaultArgs = makeDefaultArgs(); - (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ - id: '1234', - title: 'My cool doc', - expression: 'valid expression', - state: { - query: 'kuery', - }, - } as jest.ResolvedValue); - }); - function getButton(inst: ReactWrapper): TopNavMenuData { return (inst .find('[data-test-subj="lnsApp_topNav"]') @@ -495,135 +513,195 @@ describe('Lens App', () => { filters: [], }, }, - initialDocId, + initialSavedObjectId, ...saveProps }: SaveProps & { lastKnownDoc?: object; - initialDocId?: string; + initialSavedObjectId?: string; }) { - const args = { - ...defaultArgs, - docId: initialDocId, + const props = { + ...makeDefaultProps(), + initialInput: initialSavedObjectId + ? { savedObjectId: initialSavedObjectId, id: '5678' } + : undefined, }; - args.editorFrame = frame; - (args.docStorage.load as jest.Mock).mockResolvedValue({ - id: '1234', + + const services = makeDefaultServices(); + services.attributeService.wrapAttributes = jest + .fn() + .mockImplementation(async ({ savedObjectId }) => ({ + savedObjectId: savedObjectId || 'aaa', + })); + services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + savedObjectId: initialSavedObjectId ?? 'aaa', references: [], state: { query: 'fake query', filters: [], }, - }); - (args.docStorage.save as jest.Mock).mockImplementation(async ({ id }) => ({ - id: id || 'aaa', - })); + } as jest.ResolvedValue); + let frame: jest.Mocked = {} as jest.Mocked; + let component: ReactWrapper = {} as ReactWrapper; await act(async () => { - instance = mount(); + const { frame: newFrame, component: newComponent } = mountWith({ services, props }); + frame = newFrame; + component = newComponent; }); - if (initialDocId) { - expect(args.docStorage.load).toHaveBeenCalledTimes(1); + if (initialSavedObjectId) { + expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1); } else { - expect(args.docStorage.load).not.toHaveBeenCalled(); + expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled(); } const onChange = frame.mount.mock.calls[0][1].onChange; + act(() => onChange({ filterableIndexPatterns: [], - doc: { id: initialDocId, ...lastKnownDoc } as Document, + doc: { savedObjectId: initialSavedObjectId, ...lastKnownDoc } as Document, isSaveable: true, }) ); - - instance.update(); - - expect(getButton(instance).disableButton).toEqual(false); - + component.update(); + expect(getButton(component).disableButton).toEqual(false); await act(async () => { - testSave(instance, { ...saveProps }); + testSave(component, { ...saveProps }); }); - - return { args, instance }; + return { props, services, component, frame }; } it('shows a disabled save button when the user does not have permissions', async () => { - const args = defaultArgs; - args.core.application = { - ...args.core.application, + const services = makeDefaultServices(); + services.application = { + ...services.application, capabilities: { - ...args.core.application.capabilities, + ...services.application.capabilities, visualize: { save: false, saveQuery: false, show: true }, }, }; - args.editorFrame = frame; - - instance = mount(); - - expect(getButton(instance).disableButton).toEqual(true); - + const { component, frame } = mountWith({ services }); + expect(getButton(component).disableButton).toEqual(true); const onChange = frame.mount.mock.calls[0][1].onChange; act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: 'will save this' } as unknown) as Document, + doc: ({ savedObjectId: 'will save this' } as unknown) as Document, isSaveable: true, }) ); - instance.update(); - expect(getButton(instance).disableButton).toEqual(true); + component.update(); + expect(getButton(component).disableButton).toEqual(true); }); - it('shows a save button that is enabled when the frame has provided its state', async () => { - const args = defaultArgs; - args.editorFrame = frame; - - instance = mount(); - - expect(getButton(instance).disableButton).toEqual(true); - + it('shows a save button that is enabled when the frame has provided its state and does not show save and return or save as', async () => { + const { component, frame } = mountWith({}); + expect(getButton(component).disableButton).toEqual(true); const onChange = frame.mount.mock.calls[0][1].onChange; act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: 'will save this' } as unknown) as Document, + doc: ({ savedObjectId: 'will save this' } as unknown) as Document, isSaveable: true, }) ); - instance.update(); + component.update(); + expect(getButton(component).disableButton).toEqual(false); - expect(getButton(instance).disableButton).toEqual(false); + await act(async () => { + const topNavMenuConfig = component.find(TopNavMenu).prop('config'); + expect(topNavMenuConfig).not.toContainEqual( + expect.objectContaining(navMenuItems.expectedSaveAndReturnButton) + ); + expect(topNavMenuConfig).not.toContainEqual( + expect.objectContaining(navMenuItems.expectedSaveAsButton) + ); + expect(topNavMenuConfig).toContainEqual( + expect.objectContaining(navMenuItems.expectedSaveButton) + ); + }); + }); + + it('Shows Save and Return and Save As buttons in create by value mode', async () => { + const props = makeDefaultProps(); + const services = makeDefaultServices(); + services.dashboardFeatureFlag = { allowByValueEmbeddables: true }; + props.incomingState = { + originatingApp: 'ultraDashboard', + valueInput: { + id: 'whatchaGonnaDoWith', + attributes: { + title: + 'whatcha gonna do with all these references? All these references in your value Input', + references: [] as SavedObjectReference[], + }, + } as LensByValueInput, + }; + + const { component } = mountWith({ props, services }); + + await act(async () => { + const topNavMenuConfig = component.find(TopNavMenu).prop('config'); + expect(topNavMenuConfig).toContainEqual( + expect.objectContaining(navMenuItems.expectedSaveAndReturnButton) + ); + expect(topNavMenuConfig).toContainEqual( + expect.objectContaining(navMenuItems.expectedSaveAsButton) + ); + expect(topNavMenuConfig).not.toContainEqual( + expect.objectContaining(navMenuItems.expectedSaveButton) + ); + }); + }); + + it('Shows Save and Return and Save As buttons in edit by reference mode', async () => { + const props = makeDefaultProps(); + props.initialInput = { savedObjectId: defaultSavedObjectId, id: '5678' }; + props.incomingState = { + originatingApp: 'ultraDashboard', + }; + + const { component } = mountWith({ props }); + + await act(async () => { + const topNavMenuConfig = component.find(TopNavMenu).prop('config'); + expect(topNavMenuConfig).toContainEqual( + expect.objectContaining(navMenuItems.expectedSaveAndReturnButton) + ); + expect(topNavMenuConfig).toContainEqual( + expect.objectContaining(navMenuItems.expectedSaveAsButton) + ); + expect(topNavMenuConfig).not.toContainEqual( + expect.objectContaining(navMenuItems.expectedSaveButton) + ); + }); }); it('saves new docs', async () => { - const { args, instance: inst } = await save({ - initialDocId: undefined, + const { props, services } = await save({ + initialSavedObjectId: undefined, newCopyOnSave: false, newTitle: 'hello there', }); - - expect(args.docStorage.save).toHaveBeenCalledWith( + expect(services.attributeService.wrapAttributes).toHaveBeenCalledWith( expect.objectContaining({ - id: undefined, + savedObjectId: undefined, title: 'hello there', - }) + }), + true, + undefined ); - - expect(args.redirectTo).toHaveBeenCalledWith('aaa', undefined, true); - - inst.setProps({ docId: 'aaa' }); - - expect(args.docStorage.load).not.toHaveBeenCalled(); + expect(props.redirectTo).toHaveBeenCalledWith('aaa'); }); - it('adds to the recently viewed list on save', async () => { - const { args } = await save({ - initialDocId: undefined, + it('adds to the recently accessed list on save', async () => { + const { services } = await save({ + initialSavedObjectId: undefined, newCopyOnSave: false, newTitle: 'hello there', }); - expect(args.core.chrome.recentlyAccessed.add).toHaveBeenCalledWith( + expect(services.chrome.recentlyAccessed.add).toHaveBeenCalledWith( '/app/lens#/edit/aaa', 'hello there', 'aaa' @@ -631,54 +709,53 @@ describe('Lens App', () => { }); it('saves the latest doc as a copy', async () => { - const { args, instance: inst } = await save({ - initialDocId: '1234', + const { props, services, component } = await save({ + initialSavedObjectId: defaultSavedObjectId, newCopyOnSave: true, newTitle: 'hello there', }); - - expect(args.docStorage.save).toHaveBeenCalledWith( + expect(services.attributeService.wrapAttributes).toHaveBeenCalledWith( expect.objectContaining({ - id: undefined, + savedObjectId: undefined, title: 'hello there', - }) + }), + true, + undefined ); - - expect(args.redirectTo).toHaveBeenCalledWith('aaa', undefined, true); - - inst.setProps({ docId: 'aaa' }); - - expect(args.docStorage.load).toHaveBeenCalledTimes(1); + expect(props.redirectTo).toHaveBeenCalledWith('aaa'); + await act(async () => { + component.setProps({ initialInput: { savedObjectId: 'aaa' } }); + }); + expect(services.attributeService.wrapAttributes).toHaveBeenCalledTimes(1); }); it('saves existing docs', async () => { - const { args, instance: inst } = await save({ - initialDocId: '1234', + const { props, services, component } = await save({ + initialSavedObjectId: defaultSavedObjectId, newCopyOnSave: false, newTitle: 'hello there', }); - - expect(args.docStorage.save).toHaveBeenCalledWith( + expect(services.attributeService.wrapAttributes).toHaveBeenCalledWith( expect.objectContaining({ - id: '1234', + savedObjectId: defaultSavedObjectId, title: 'hello there', - }) + }), + true, + { id: '5678', savedObjectId: defaultSavedObjectId } ); - - expect(args.redirectTo).not.toHaveBeenCalled(); - - inst.setProps({ docId: '1234' }); - - expect(args.docStorage.load).toHaveBeenCalledTimes(1); + expect(props.redirectTo).not.toHaveBeenCalled(); + await act(async () => { + component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); + }); + expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1); }); it('handles save failure by showing a warning, but still allows another save', async () => { - const args = defaultArgs; - args.editorFrame = frame; - (args.docStorage.save as jest.Mock).mockRejectedValue({ message: 'failed' }); - - instance = mount(); - + const services = makeDefaultServices(); + services.attributeService.wrapAttributes = jest + .fn() + .mockRejectedValue({ message: 'failed' }); + const { component, props, frame } = mountWith({ services }); const onChange = frame.mount.mock.calls[0][1].onChange; act(() => onChange({ @@ -687,51 +764,48 @@ describe('Lens App', () => { isSaveable: true, }) ); - - instance.update(); + component.update(); await act(async () => { - testSave(instance, { newCopyOnSave: false, newTitle: 'hello there' }); + testSave(component, { newCopyOnSave: false, newTitle: 'hello there' }); }); - - expect(args.core.notifications.toasts.addDanger).toHaveBeenCalled(); - expect(args.redirectTo).not.toHaveBeenCalled(); - - expect(getButton(instance).disableButton).toEqual(false); + expect(services.notifications.toasts.addDanger).toHaveBeenCalled(); + expect(props.redirectTo).not.toHaveBeenCalled(); + expect(getButton(component).disableButton).toEqual(false); }); it('saves new doc and redirects to originating app', async () => { - const { args } = await save({ - initialDocId: undefined, + const { props, services } = await save({ + initialSavedObjectId: undefined, returnToOrigin: true, newCopyOnSave: false, newTitle: 'hello there', }); - - expect(args.docStorage.save).toHaveBeenCalledWith( + expect(services.attributeService.wrapAttributes).toHaveBeenCalledWith( expect.objectContaining({ - id: undefined, + savedObjectId: undefined, title: 'hello there', - }) + }), + true, + undefined ); - - expect(args.redirectTo).toHaveBeenCalledWith('aaa', true, true); + expect(props.redirectToOrigin).toHaveBeenCalledWith({ + input: { savedObjectId: 'aaa' }, + isCopied: false, + }); }); it('saves app filters and does not save pinned filters', async () => { const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; - const unpinned = esFilters.buildExistsFilter(field, indexPattern); const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); - await act(async () => { FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); }); - - const { args } = await save({ - initialDocId: '1234', + const { services } = await save({ + initialSavedObjectId: defaultSavedObjectId, newCopyOnSave: false, newTitle: 'hello there2', lastKnownDoc: { @@ -741,42 +815,42 @@ describe('Lens App', () => { }, }, }); - - expect(args.docStorage.save).toHaveBeenCalledWith({ - id: '1234', - title: 'hello there2', - expression: 'kibana 3', - state: { - filters: [unpinned], + expect(services.attributeService.wrapAttributes).toHaveBeenCalledWith( + { + savedObjectId: defaultSavedObjectId, + title: 'hello there2', + expression: 'kibana 3', + state: { + filters: [unpinned], + }, }, - }); + true, + { id: '5678', savedObjectId: defaultSavedObjectId } + ); }); it('checks for duplicate title before saving', async () => { - const args = defaultArgs; - args.editorFrame = frame; - (args.docStorage.save as jest.Mock).mockReturnValue(Promise.resolve({ id: '123' })); - - instance = mount(); - + const services = makeDefaultServices(); + services.attributeService.wrapAttributes = jest + .fn() + .mockReturnValue(Promise.resolve({ savedObjectId: '123' })); + const { component, frame } = mountWith({ services }); const onChange = frame.mount.mock.calls[0][1].onChange; await act(async () => onChange({ filterableIndexPatterns: [], - doc: ({ id: '123' } as unknown) as Document, + doc: ({ savedObjectId: '123' } as unknown) as Document, isSaveable: true, }) ); - instance.update(); + component.update(); await act(async () => { - getButton(instance).run(instance.getDOMNode()); + getButton(component).run(component.getDOMNode()); }); - instance.update(); - + component.update(); const onTitleDuplicate = jest.fn(); - await act(async () => { - instance.find(SavedObjectSaveModal).prop('onSave')({ + component.find(SavedObjectSaveModal).prop('onSave')({ onTitleDuplicate, isTitleDuplicateConfirmed: false, newCopyOnSave: false, @@ -784,9 +858,8 @@ describe('Lens App', () => { newTitle: 'test', }); }); - expect(checkForDuplicateTitle).toHaveBeenCalledWith( - expect.objectContaining({ id: '123' }), + expect.objectContaining({ savedObjectId: '123' }), false, onTitleDuplicate, expect.anything() @@ -794,11 +867,7 @@ describe('Lens App', () => { }); it('does not show the copy button on first save', async () => { - const args = defaultArgs; - args.editorFrame = frame; - - instance = mount(); - + const { component, frame } = mountWith({}); const onChange = frame.mount.mock.calls[0][1].onChange; await act(async () => onChange({ @@ -807,36 +876,17 @@ describe('Lens App', () => { isSaveable: true, }) ); - instance.update(); - await act(async () => getButton(instance).run(instance.getDOMNode())); - instance.update(); - - expect(instance.find(SavedObjectSaveModal).prop('showCopyOnSave')).toEqual(false); + component.update(); + await act(async () => getButton(component).run(component.getDOMNode())); + component.update(); + expect(component.find(SavedObjectSaveModal).prop('showCopyOnSave')).toEqual(false); }); }); }); describe('query bar state management', () => { - let defaultArgs: ReturnType; - - beforeEach(() => { - defaultArgs = makeDefaultArgs(); - (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ - id: '1234', - title: 'My cool doc', - expression: 'valid expression', - state: { - query: 'kuery', - }, - } as jest.ResolvedValue); - }); - it('uses the default time and query language settings', () => { - const args = defaultArgs; - args.editorFrame = frame; - - mount(); - + const { frame } = mountWith({}); expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ query: { query: '', language: 'kuery' }, @@ -855,20 +905,14 @@ describe('Lens App', () => { }); it('updates the index patterns when the editor frame is changed', async () => { - const args = defaultArgs; - args.editorFrame = frame; - - instance = mount(); - + const { component, frame } = mountWith({}); expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ indexPatterns: [], }), {} ); - const onChange = frame.mount.mock.calls[0][1].onChange; - await act(async () => { onChange({ filterableIndexPatterns: ['1'], @@ -876,18 +920,14 @@ describe('Lens App', () => { isSaveable: true, }); }); - - instance.update(); - + component.update(); expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ indexPatterns: [{ id: '1' }], }), {} ); - // Do it again to verify that the dirty checking is done right - await act(async () => { onChange({ filterableIndexPatterns: ['2'], @@ -895,9 +935,7 @@ describe('Lens App', () => { isSaveable: true, }); }); - - instance.update(); - + component.update(); expect(TopNavMenu).toHaveBeenLastCalledWith( expect.objectContaining({ indexPatterns: [{ id: '2' }], @@ -905,21 +943,16 @@ describe('Lens App', () => { {} ); }); - it('updates the editor frame when the user changes query or time in the search bar', () => { - const args = defaultArgs; - args.editorFrame = frame; - - instance = mount(); + it('updates the editor frame when the user changes query or time in the search bar', () => { + const { component, frame } = mountWith({}); act(() => - instance.find(TopNavMenu).prop('onQuerySubmit')!({ + component.find(TopNavMenu).prop('onQuerySubmit')!({ dateRange: { from: 'now-14d', to: 'now-7d' }, query: { query: 'new', language: 'lucene' }, }) ); - - instance.update(); - + component.update(); expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ query: { query: 'new', language: 'lucene' }, @@ -938,19 +971,15 @@ describe('Lens App', () => { }); it('updates the filters when the user changes them', () => { - const args = defaultArgs; - args.editorFrame = frame; - - instance = mount(); + const { component, frame, services } = mountWith({}); const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; - act(() => - args.data.query.filterManager.setFilters([esFilters.buildExistsFilter(field, indexPattern)]) + services.data.query.filterManager.setFilters([ + esFilters.buildExistsFilter(field, indexPattern), + ]) ); - - instance.update(); - + component.update(); expect(frame.mount).toHaveBeenCalledWith( expect.any(Element), expect.objectContaining({ @@ -962,17 +991,15 @@ describe('Lens App', () => { describe('saved query handling', () => { it('does not allow saving when the user is missing the saveQuery permission', () => { - const args = makeDefaultArgs(); - args.core.application = { - ...args.core.application, + const services = makeDefaultServices(); + services.application = { + ...services.application, capabilities: { - ...args.core.application.capabilities, + ...services.application.capabilities, visualize: { save: false, saveQuery: false, show: true }, }, }; - - mount(); - + mountWith({ services }); expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ showSaveQuery: false }), {} @@ -980,11 +1007,7 @@ describe('Lens App', () => { }); it('persists the saved query ID when the query is saved', () => { - const args = makeDefaultArgs(); - args.editorFrame = frame; - - instance = mount(); - + const { component } = mountWith({}); expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ showSaveQuery: true, @@ -995,9 +1018,8 @@ describe('Lens App', () => { }), {} ); - act(() => { - instance.find(TopNavMenu).prop('onSaved')!({ + component.find(TopNavMenu).prop('onSaved')!({ id: '1', attributes: { title: '', @@ -1006,7 +1028,6 @@ describe('Lens App', () => { }, }); }); - expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ savedQuery: { @@ -1023,13 +1044,9 @@ describe('Lens App', () => { }); it('changes the saved query ID when the query is updated', () => { - const args = makeDefaultArgs(); - args.editorFrame = frame; - - instance = mount(); - + const { component } = mountWith({}); act(() => { - instance.find(TopNavMenu).prop('onSaved')!({ + component.find(TopNavMenu).prop('onSaved')!({ id: '1', attributes: { title: '', @@ -1038,9 +1055,8 @@ describe('Lens App', () => { }, }); }); - act(() => { - instance.find(TopNavMenu).prop('onSavedQueryUpdated')!({ + component.find(TopNavMenu).prop('onSavedQueryUpdated')!({ id: '2', attributes: { title: 'new title', @@ -1049,7 +1065,6 @@ describe('Lens App', () => { }, }); }); - expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ savedQuery: { @@ -1066,32 +1081,23 @@ describe('Lens App', () => { }); it('clears all existing unpinned filters when the active saved query is cleared', () => { - const args = makeDefaultArgs(); - args.editorFrame = frame; - - instance = mount(); - + const { component, frame, services } = mountWith({}); act(() => - instance.find(TopNavMenu).prop('onQuerySubmit')!({ + component.find(TopNavMenu).prop('onQuerySubmit')!({ dateRange: { from: 'now-14d', to: 'now-7d' }, query: { query: 'new', language: 'lucene' }, }) ); - const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; - const unpinned = esFilters.buildExistsFilter(field, indexPattern); const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); - - act(() => args.data.query.filterManager.setFilters([pinned, unpinned])); - instance.update(); - - act(() => instance.find(TopNavMenu).prop('onClearSavedQuery')!()); - instance.update(); - + act(() => services.data.query.filterManager.setFilters([pinned, unpinned])); + component.update(); + act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!()); + component.update(); expect(frame.mount).toHaveBeenLastCalledWith( expect.any(Element), expect.objectContaining({ @@ -1101,191 +1107,127 @@ describe('Lens App', () => { }); }); - it('displays errors from the frame in a toast', () => { - const args = makeDefaultArgs(); - args.editorFrame = frame; - - instance = mount(); - - const onError = frame.mount.mock.calls[0][1].onError; - onError({ message: 'error' }); - - instance.update(); - - expect(args.core.notifications.toasts.addDanger).toHaveBeenCalled(); - }); - describe('showing a confirm message when leaving', () => { - let defaultArgs: ReturnType; let defaultLeave: jest.Mock; let confirmLeave: jest.Mock; beforeEach(() => { - defaultArgs = makeDefaultArgs(); defaultLeave = jest.fn(); confirmLeave = jest.fn(); - (defaultArgs.docStorage.load as jest.Mock).mockResolvedValue({ - id: '1234', - title: 'My cool doc', - state: { - query: 'kuery', - filters: [], - }, - references: [], - } as jest.ResolvedValue); }); it('should not show a confirm message if there is no expression to save', () => { - instance = mount(); - - const lastCall = - defaultArgs.onAppLeave.mock.calls[defaultArgs.onAppLeave.mock.calls.length - 1][0]; + const { props } = mountWith({}); + const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); - expect(defaultLeave).toHaveBeenCalled(); expect(confirmLeave).not.toHaveBeenCalled(); }); it('does not confirm if the user is missing save permissions', () => { - const args = defaultArgs; - args.core.application = { - ...args.core.application, + const services = makeDefaultServices(); + services.application = { + ...services.application, capabilities: { - ...args.core.application.capabilities, + ...services.application.capabilities, visualize: { save: false, saveQuery: false, show: true }, }, }; - args.editorFrame = frame; - - instance = mount(); - + const { component, frame, props } = mountWith({ services }); const onChange = frame.mount.mock.calls[0][1].onChange; act(() => onChange({ filterableIndexPatterns: [], doc: ({ - id: undefined, - + savedObjectId: undefined, references: [], } as unknown) as Document, isSaveable: true, }) ); - instance.update(); - - const lastCall = - defaultArgs.onAppLeave.mock.calls[defaultArgs.onAppLeave.mock.calls.length - 1][0]; + component.update(); + const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); - expect(defaultLeave).toHaveBeenCalled(); expect(confirmLeave).not.toHaveBeenCalled(); }); it('should confirm when leaving with an unsaved doc', () => { - defaultArgs.editorFrame = frame; - instance = mount(); - + const { component, frame, props } = mountWith({}); const onChange = frame.mount.mock.calls[0][1].onChange; act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: undefined, state: {} } as unknown) as Document, + doc: ({ savedObjectId: undefined, state: {} } as unknown) as Document, isSaveable: true, }) ); - instance.update(); - - const lastCall = - defaultArgs.onAppLeave.mock.calls[defaultArgs.onAppLeave.mock.calls.length - 1][0]; + component.update(); + const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); - expect(confirmLeave).toHaveBeenCalled(); expect(defaultLeave).not.toHaveBeenCalled(); }); it('should confirm when leaving with unsaved changes to an existing doc', async () => { - defaultArgs.editorFrame = frame; - instance = mount(); + const { component, frame, props } = mountWith({}); await act(async () => { - instance.setProps({ docId: '1234' }); + component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); }); - const onChange = frame.mount.mock.calls[0][1].onChange; act(() => onChange({ filterableIndexPatterns: [], doc: ({ - id: '1234', - + savedObjectId: defaultSavedObjectId, references: [], } as unknown) as Document, isSaveable: true, }) ); - instance.update(); - - const lastCall = - defaultArgs.onAppLeave.mock.calls[defaultArgs.onAppLeave.mock.calls.length - 1][0]; + component.update(); + const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); - expect(confirmLeave).toHaveBeenCalled(); expect(defaultLeave).not.toHaveBeenCalled(); }); it('should not confirm when changes are saved', async () => { - defaultArgs.editorFrame = frame; - instance = mount(); + const { component, frame, props } = mountWith({}); await act(async () => { - instance.setProps({ docId: '1234' }); + component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); }); - const onChange = frame.mount.mock.calls[0][1].onChange; act(() => onChange({ filterableIndexPatterns: [], - doc: ({ - id: '1234', - title: 'My cool doc', - references: [], - state: { - query: 'kuery', - filters: [], - }, - } as unknown) as Document, + doc: defaultDoc, isSaveable: true, }) ); - instance.update(); - - const lastCall = - defaultArgs.onAppLeave.mock.calls[defaultArgs.onAppLeave.mock.calls.length - 1][0]; + component.update(); + const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); - expect(defaultLeave).toHaveBeenCalled(); expect(confirmLeave).not.toHaveBeenCalled(); }); it('should confirm when the latest doc is invalid', async () => { - defaultArgs.editorFrame = frame; - instance = mount(); + const { component, frame, props } = mountWith({}); await act(async () => { - instance.setProps({ docId: '1234' }); + component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); }); - const onChange = frame.mount.mock.calls[0][1].onChange; act(() => onChange({ filterableIndexPatterns: [], - doc: ({ id: '1234', references: [] } as unknown) as Document, + doc: ({ savedObjectId: defaultSavedObjectId, references: [] } as unknown) as Document, isSaveable: true, }) ); - instance.update(); - - const lastCall = - defaultArgs.onAppLeave.mock.calls[defaultArgs.onAppLeave.mock.calls.length - 1][0]; + component.update(); + const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); - expect(confirmLeave).toHaveBeenCalled(); expect(defaultLeave).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index bfdf4ceaaabd3..d2ccbe0cb2fee 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -6,108 +6,82 @@ import _ from 'lodash'; import React, { useState, useEffect, useCallback } from 'react'; -import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; -import { AppMountContext, AppMountParameters, NotificationsStart } from 'kibana/public'; -import { History } from 'history'; +import { NotificationsStart } from 'kibana/public'; import { EuiBreadcrumb } from '@elastic/eui'; -import { - Query, - DataPublicPluginStart, - syncQueryStateWithUrl, -} from '../../../../../src/plugins/data/public'; import { createKbnUrlStateStorage, - IStorageWrapper, withNotifyOnErrors, } from '../../../../../src/plugins/kibana_utils/public'; -import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { - SavedObjectSaveModalOrigin, OnSaveProps, checkForDuplicateTitle, + SavedObjectSaveModalOrigin, } from '../../../../../src/plugins/saved_objects/public'; -import { Document, SavedObjectStore, injectFilterReferences } from '../persistence'; -import { EditorFrameInstance } from '../types'; +import { injectFilterReferences } from '../persistence'; import { NativeRenderer } from '../native_renderer'; import { trackUiEvent } from '../lens_ui_telemetry'; import { esFilters, - Filter, IndexPattern as IndexPatternInstance, IndexPatternsContract, - SavedQuery, + syncQueryStateWithUrl, } from '../../../../../src/plugins/data/public'; -import { getFullPath } from '../../common'; - -interface State { - indicateNoData: boolean; - isLoading: boolean; - isSaveModalVisible: boolean; - indexPatternsForTopNav: IndexPatternInstance[]; - originatingApp?: string; - persistedDoc?: Document; - lastKnownDoc?: Document; - - // Properties needed to interface with TopNav - dateRange: { - fromDate: string; - toDate: string; - }; - query: Query; - filters: Filter[]; - savedQuery?: SavedQuery; - isSaveable: boolean; -} +import { LENS_EMBEDDABLE_TYPE, getFullPath } from '../../common'; +import { LensAppProps, LensAppServices, LensAppState } from './types'; +import { getLensTopNavConfig } from './lens_top_nav'; +import { + LensByReferenceInput, + LensEmbeddableInput, +} from '../editor_frame_service/embeddable/embeddable'; export function App({ - editorFrame, - data, - core, - storage, - docId, - docStorage, - redirectTo, - originatingApp, - navigation, + history, onAppLeave, + redirectTo, + editorFrame, + initialInput, + incomingState, + redirectToOrigin, setHeaderActionMenu, - history, - getAppNameFromId, -}: { - editorFrame: EditorFrameInstance; - data: DataPublicPluginStart; - navigation: NavigationPublicPluginStart; - core: AppMountContext['core']; - storage: IStorageWrapper; - docId?: string; - docStorage: SavedObjectStore; - redirectTo: (id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => void; - originatingApp?: string | undefined; - onAppLeave: AppMountParameters['onAppLeave']; - setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; - history: History; - getAppNameFromId?: (appId: string) => string | undefined; -}) { - const [state, setState] = useState(() => { +}: LensAppProps) { + const { + data, + chrome, + overlays, + navigation, + uiSettings, + application, + notifications, + attributeService, + savedObjectsClient, + getOriginatingAppName, + + // Temporarily required until the 'by value' paradigm is default. + dashboardFeatureFlag, + } = useKibana().services; + + const [state, setState] = useState(() => { const currentRange = data.query.timefilter.timefilter.getTime(); return { - isLoading: !!docId, - isSaveModalVisible: false, - indexPatternsForTopNav: [], query: data.query.queryString.getDefaultQuery(), + filters: data.query.filterManager.getFilters(), + isLoading: Boolean(initialInput), + indexPatternsForTopNav: [], dateRange: { fromDate: currentRange.from, toDate: currentRange.to, }, - originatingApp, - filters: data.query.filterManager.getFilters(), + isLinkedToOriginatingApp: Boolean(incomingState?.originatingApp), + isSaveModalVisible: false, indicateNoData: false, isSaveable: false, }; }); + const { lastKnownDoc } = state; + const showNoDataPopover = useCallback(() => { setState((prevState) => ({ ...prevState, indicateNoData: true })); }, [setState]); @@ -125,9 +99,44 @@ export function App({ state.indexPatternsForTopNav, ]); - const { lastKnownDoc } = state; + const onError = useCallback( + (e: { message: string }) => + notifications.toasts.addDanger({ + title: e.message, + }), + [notifications.toasts] + ); + + const getLastKnownDocWithoutPinnedFilters = useCallback( + function () { + if (!lastKnownDoc) return undefined; + const [pinnedFilters, appFilters] = _.partition( + injectFilterReferences(lastKnownDoc.state?.filters || [], lastKnownDoc.references), + esFilters.isFilterPinned + ); + return pinnedFilters?.length + ? { + ...lastKnownDoc, + state: { + ...lastKnownDoc.state, + filters: appFilters, + }, + } + : lastKnownDoc; + }, + [lastKnownDoc] + ); - const savingPermitted = state.isSaveable && core.application.capabilities.visualize.save; + const getIsByValueMode = useCallback( + () => + Boolean( + // Temporarily required until the 'by value' paradigm is default. + dashboardFeatureFlag.allowByValueEmbeddables && + state.isLinkedToOriginatingApp && + !(initialInput as LensByReferenceInput)?.savedObjectId + ), + [dashboardFeatureFlag.allowByValueEmbeddables, state.isLinkedToOriginatingApp, initialInput] + ); useEffect(() => { // Clear app-specific filters when navigating to Lens. Necessary because Lens @@ -156,8 +165,8 @@ export function App({ const kbnUrlStateStorage = createKbnUrlStateStorage({ history, - useHash: core.uiSettings.get('state:storeInSessionStorage'), - ...withNotifyOnErrors(core.notifications.toasts), + useHash: uiSettings.get('state:storeInSessionStorage'), + ...withNotifyOnErrors(notifications.toasts), }); const { stop: stopSyncingQueryServiceStateWithUrl } = syncQueryStateWithUrl( data.query, @@ -172,38 +181,18 @@ export function App({ }, [ data.query.filterManager, data.query.timefilter.timefilter, - core.notifications.toasts, - core.uiSettings, + notifications.toasts, + uiSettings, data.query, history, ]); - const getLastKnownDocWithoutPinnedFilters = useCallback( - function () { - if (!lastKnownDoc) return undefined; - const [pinnedFilters, appFilters] = _.partition( - injectFilterReferences(lastKnownDoc.state?.filters || [], lastKnownDoc.references), - esFilters.isFilterPinned - ); - return pinnedFilters?.length - ? { - ...lastKnownDoc, - state: { - ...lastKnownDoc.state, - filters: appFilters, - }, - } - : lastKnownDoc; - }, - [lastKnownDoc] - ); - useEffect(() => { onAppLeave((actions) => { // Confirm when the user has made any changes to an existing doc // or when the user has configured something without saving if ( - core.application.capabilities.visualize.save && + application.capabilities.visualize.save && !_.isEqual(state.persistedDoc?.state, getLastKnownDocWithoutPinnedFilters()?.state) && (state.isSaveable || state.persistedDoc) ) { @@ -220,379 +209,430 @@ export function App({ } }); }, [ - lastKnownDoc, onAppLeave, - state.persistedDoc, + lastKnownDoc, state.isSaveable, - core.application.capabilities.visualize.save, + state.persistedDoc, getLastKnownDocWithoutPinnedFilters, + application.capabilities.visualize.save, ]); // Sync Kibana breadcrumbs any time the saved document's title changes useEffect(() => { - core.chrome.setBreadcrumbs([ - ...(originatingApp && getAppNameFromId - ? [ - { - onClick: (e) => { - core.application.navigateToApp(originatingApp); - }, - text: getAppNameFromId(originatingApp), - } as EuiBreadcrumb, - ] - : []), - { - href: core.http.basePath.prepend(`/app/visualize#/`), + const isByValueMode = getIsByValueMode(); + const breadcrumbs: EuiBreadcrumb[] = []; + if (state.isLinkedToOriginatingApp && getOriginatingAppName() && redirectToOrigin) { + breadcrumbs.push({ + onClick: () => { + redirectToOrigin(); + }, + text: getOriginatingAppName(), + }); + } + if (!isByValueMode) { + breadcrumbs.push({ + href: application.getUrlForApp('visualize'), onClick: (e) => { - core.application.navigateToApp('visualize', { path: '/' }); + application.navigateToApp('visualize', { path: '/' }); e.preventDefault(); }, text: i18n.translate('xpack.lens.breadcrumbsTitle', { defaultMessage: 'Visualize', }), - }, - { - text: state.persistedDoc - ? state.persistedDoc.title - : i18n.translate('xpack.lens.breadcrumbsCreate', { defaultMessage: 'Create' }), - }, - ]); + }); + } + let currentDocTitle = i18n.translate('xpack.lens.breadcrumbsCreate', { + defaultMessage: 'Create', + }); + if (state.persistedDoc) { + currentDocTitle = isByValueMode + ? i18n.translate('xpack.lens.breadcrumbsByValue', { defaultMessage: 'Edit visualization' }) + : state.persistedDoc.title; + } + breadcrumbs.push({ text: currentDocTitle }); + chrome.setBreadcrumbs(breadcrumbs); }, [ - core.application, - core.chrome, - core.http.basePath, + dashboardFeatureFlag.allowByValueEmbeddables, + state.isLinkedToOriginatingApp, + getOriginatingAppName, state.persistedDoc, - originatingApp, - redirectTo, - getAppNameFromId, + redirectToOrigin, + getIsByValueMode, + initialInput, + application, + chrome, ]); - useEffect( - () => { - if (docId && (!state.persistedDoc || state.persistedDoc.id !== docId)) { - setState((s) => ({ ...s, isLoading: true })); - docStorage - .load(docId) - .then((doc) => { - core.chrome.recentlyAccessed.add(getFullPath(docId), doc.title, docId); - getAllIndexPatterns( - _.uniq( - doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id) - ), - data.indexPatterns, - core.notifications - ) - .then((indexPatterns) => { - // Don't overwrite any pinned filters - data.query.filterManager.setAppFilters( - injectFilterReferences(doc.state.filters, doc.references) - ); - setState((s) => ({ - ...s, - isLoading: false, - persistedDoc: doc, - lastKnownDoc: doc, - query: doc.state.query, - indexPatternsForTopNav: indexPatterns, - })); - }) - .catch((e) => { - setState((s) => ({ ...s, isLoading: false })); - - redirectTo(); - }); + useEffect(() => { + if ( + !initialInput || + (attributeService.inputIsRefType(initialInput) && + initialInput.savedObjectId === state.persistedDoc?.savedObjectId) + ) { + return; + } + + setState((s) => ({ ...s, isLoading: true })); + attributeService + .unwrapAttributes(initialInput) + .then((attributes) => { + if (!initialInput) { + return; + } + const doc = { + ...initialInput, + ...attributes, + type: LENS_EMBEDDABLE_TYPE, + }; + + if (attributeService.inputIsRefType(initialInput)) { + chrome.recentlyAccessed.add( + getFullPath(initialInput.savedObjectId), + attributes.title, + initialInput.savedObjectId + ); + } + getAllIndexPatterns( + _.uniq(doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id)), + data.indexPatterns, + notifications + ) + .then((indexPatterns) => { + // Don't overwrite any pinned filters + data.query.filterManager.setAppFilters( + injectFilterReferences(doc.state.filters, doc.references) + ); + setState((s) => ({ + ...s, + isLoading: false, + persistedDoc: doc, + lastKnownDoc: doc, + query: doc.state.query, + indexPatternsForTopNav: indexPatterns, + })); }) .catch((e) => { setState((s) => ({ ...s, isLoading: false })); - - core.notifications.toasts.addDanger( - i18n.translate('xpack.lens.app.docLoadingError', { - defaultMessage: 'Error loading saved document', - }) - ); - redirectTo(); }); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - core.notifications, - data.indexPatterns, - data.query.filterManager, - docId, - // TODO: These dependencies are changing too often - // docStorage, - // redirectTo, - // state.persistedDoc, - ] - ); + }) + .catch((e) => { + setState((s) => ({ ...s, isLoading: false })); + notifications.toasts.addDanger( + i18n.translate('xpack.lens.app.docLoadingError', { + defaultMessage: 'Error loading saved document', + }) + ); + + redirectTo(); + }); + }, [ + notifications, + data.indexPatterns, + data.query.filterManager, + initialInput, + attributeService, + redirectTo, + chrome.recentlyAccessed, + state.persistedDoc?.savedObjectId, + state.persistedDoc?.state, + ]); const runSave = async ( saveProps: Omit & { returnToOrigin: boolean; onTitleDuplicate?: OnSaveProps['onTitleDuplicate']; newDescription?: string; - } + }, + options: { saveToLibrary: boolean } ) => { if (!lastKnownDoc) { return; } - - const doc = { + const docToSave = { ...getLastKnownDocWithoutPinnedFilters()!, description: saveProps.newDescription, - id: saveProps.newCopyOnSave ? undefined : lastKnownDoc.id, + savedObjectId: saveProps.newCopyOnSave ? undefined : lastKnownDoc.savedObjectId, title: saveProps.newTitle, }; - await checkForDuplicateTitle( - { - ...doc, - copyOnSave: saveProps.newCopyOnSave, - lastSavedTitle: lastKnownDoc?.title, - getEsType: () => 'lens', - getDisplayName: () => - i18n.translate('xpack.lens.app.saveModalType', { - defaultMessage: 'Lens visualization', - }), - }, - saveProps.isTitleDuplicateConfirmed, - saveProps.onTitleDuplicate, - { - savedObjectsClient: core.savedObjects.client, - overlays: core.overlays, + // Required to serialize filters in by value mode until + // https://github.com/elastic/kibana/issues/77588 is fixed + if (getIsByValueMode()) { + docToSave.state.filters.forEach((filter) => { + if (typeof filter.meta.value === 'function') { + delete filter.meta.value; + } + }); + } + + const originalInput = saveProps.newCopyOnSave ? undefined : initialInput; + const originalSavedObjectId = (originalInput as LensByReferenceInput)?.savedObjectId; + if (options.saveToLibrary && !originalInput) { + await checkForDuplicateTitle( + { + ...docToSave, + copyOnSave: saveProps.newCopyOnSave, + lastSavedTitle: lastKnownDoc.title, + getEsType: () => 'lens', + getDisplayName: () => + i18n.translate('xpack.lens.app.saveModalType', { + defaultMessage: 'Lens visualization', + }), + }, + saveProps.isTitleDuplicateConfirmed, + saveProps.onTitleDuplicate, + { + savedObjectsClient, + overlays, + } + ); + } + try { + const newInput = (await attributeService.wrapAttributes( + docToSave, + options.saveToLibrary, + originalInput + )) as LensEmbeddableInput; + + if (saveProps.returnToOrigin && redirectToOrigin) { + // disabling the validation on app leave because the document has been saved. + onAppLeave((actions) => { + return actions.default(); + }); + redirectToOrigin({ input: newInput, isCopied: saveProps.newCopyOnSave }); + return; } - ); - const newlyCreated: boolean = saveProps.newCopyOnSave || !lastKnownDoc?.id; - docStorage - .save(doc) - .then(({ id }) => { - core.chrome.recentlyAccessed.add(getFullPath(id), doc.title, id); - // Prevents unnecessary network request and disables save button - const newDoc = { ...doc, id }; - const currentOriginatingApp = state.originatingApp; + if ( + attributeService.inputIsRefType(newInput) && + newInput.savedObjectId !== originalSavedObjectId + ) { + chrome.recentlyAccessed.add( + getFullPath(newInput.savedObjectId), + docToSave.title, + newInput.savedObjectId + ); setState((s) => ({ ...s, isSaveModalVisible: false, - originatingApp: - newlyCreated && !saveProps.returnToOrigin ? undefined : currentOriginatingApp, - persistedDoc: newDoc, - lastKnownDoc: newDoc, + isLinkedToOriginatingApp: false, })); - if (docId !== id || saveProps.returnToOrigin) { - redirectTo(id, saveProps.returnToOrigin, newlyCreated); - } - }) - .catch((e) => { - // eslint-disable-next-line no-console - console.dir(e); - trackUiEvent('save_failed'); - core.notifications.toasts.addDanger( - i18n.translate('xpack.lens.app.docSavingError', { - defaultMessage: 'Error saving document', - }) - ); - setState((s) => ({ ...s, isSaveModalVisible: false })); - }); - }; + redirectTo(newInput.savedObjectId); + return; + } - const onError = useCallback( - (e: { message: string }) => - core.notifications.toasts.addDanger({ - title: e.message, - }), - [core.notifications.toasts] - ); + const newDoc = { + ...docToSave, + ...newInput, + }; + setState((s) => ({ + ...s, + persistedDoc: newDoc, + lastKnownDoc: newDoc, + isSaveModalVisible: false, + isLinkedToOriginatingApp: false, + })); + } catch (e) { + // eslint-disable-next-line no-console + console.dir(e); + trackUiEvent('save_failed'); + notifications.toasts.addDanger( + i18n.translate('xpack.lens.app.docSavingError', { + defaultMessage: 'Error saving document', + }) + ); + setState((s) => ({ ...s, isSaveModalVisible: false })); + } + }; const { TopNavMenu } = navigation.ui; + const savingPermitted = Boolean(state.isSaveable && application.capabilities.visualize.save); + const topNavConfig = getLensTopNavConfig({ + showSaveAndReturn: Boolean( + state.isLinkedToOriginatingApp && + // Temporarily required until the 'by value' paradigm is default. + (dashboardFeatureFlag.allowByValueEmbeddables || Boolean(initialInput)) + ), + isByValueMode: getIsByValueMode(), + showCancel: Boolean(state.isLinkedToOriginatingApp), + savingPermitted, + actions: { + saveAndReturn: () => { + if (savingPermitted && lastKnownDoc) { + // disabling the validation on app leave because the document has been saved. + onAppLeave((actions) => { + return actions.default(); + }); + runSave( + { + newTitle: lastKnownDoc.title, + newCopyOnSave: false, + isTitleDuplicateConfirmed: false, + returnToOrigin: true, + }, + { + saveToLibrary: + (initialInput && attributeService.inputIsRefType(initialInput)) ?? false, + } + ); + } + }, + showSaveModal: () => { + if (savingPermitted) { + setState((s) => ({ ...s, isSaveModalVisible: true })); + } + }, + cancel: () => { + if (redirectToOrigin) { + redirectToOrigin(); + } + }, + }, + }); + return ( - - -
-
- { - if (savingPermitted) { - runSave({ - newTitle: lastKnownDoc.title, - newCopyOnSave: false, - isTitleDuplicateConfirmed: false, - returnToOrigin: true, - }); - } - }, - testId: 'lnsApp_saveAndReturnButton', - disableButton: !savingPermitted, - }, - ] - : []), - { - label: - lastKnownDoc?.id && !!state.originatingApp - ? i18n.translate('xpack.lens.app.saveAs', { - defaultMessage: 'Save as', - }) - : i18n.translate('xpack.lens.app.save', { - defaultMessage: 'Save', - }), - emphasize: !state.originatingApp || !lastKnownDoc?.id, - run: () => { - if (savingPermitted) { - setState((s) => ({ ...s, isSaveModalVisible: true })); - } - }, - testId: 'lnsApp_saveButton', - disableButton: !savingPermitted, + <> +
+
+ { + const { dateRange, query } = payload; + if ( + dateRange.from !== state.dateRange.fromDate || + dateRange.to !== state.dateRange.toDate + ) { + data.query.timefilter.timefilter.setTime(dateRange); + trackUiEvent('app_date_change'); + } else { + trackUiEvent('app_query_change'); + } + setState((s) => ({ + ...s, + dateRange: { + fromDate: dateRange.from, + toDate: dateRange.to, }, - ]} - data-test-subj="lnsApp_topNav" - screenTitle={'lens'} - onQuerySubmit={(payload) => { - const { dateRange, query } = payload; + query: query || s.query, + })); + }} + onSaved={(savedQuery) => { + setState((s) => ({ ...s, savedQuery })); + }} + onSavedQueryUpdated={(savedQuery) => { + const savedQueryFilters = savedQuery.attributes.filters || []; + const globalFilters = data.query.filterManager.getGlobalFilters(); + data.query.filterManager.setFilters([...globalFilters, ...savedQueryFilters]); + setState((s) => ({ + ...s, + savedQuery: { ...savedQuery }, // Shallow query for reference issues + dateRange: savedQuery.attributes.timefilter + ? { + fromDate: savedQuery.attributes.timefilter.from, + toDate: savedQuery.attributes.timefilter.to, + } + : s.dateRange, + })); + }} + onClearSavedQuery={() => { + data.query.filterManager.setFilters(data.query.filterManager.getGlobalFilters()); + setState((s) => ({ + ...s, + savedQuery: undefined, + filters: data.query.filterManager.getGlobalFilters(), + query: data.query.queryString.getDefaultQuery(), + })); + }} + query={state.query} + dateRangeFrom={state.dateRange.fromDate} + dateRangeTo={state.dateRange.toDate} + indicateNoData={state.indicateNoData} + /> +
+ {(!state.isLoading || state.persistedDoc) && ( + { + if (isSaveable !== state.isSaveable) { + setState((s) => ({ ...s, isSaveable })); + } + if (!_.isEqual(state.persistedDoc, doc)) { + setState((s) => ({ ...s, lastKnownDoc: doc })); + } + // Update the cached index patterns if the user made a change to any of them if ( - dateRange.from !== state.dateRange.fromDate || - dateRange.to !== state.dateRange.toDate + state.indexPatternsForTopNav.length !== filterableIndexPatterns.length || + filterableIndexPatterns.some( + (id) => + !state.indexPatternsForTopNav.find((indexPattern) => indexPattern.id === id) + ) ) { - data.query.timefilter.timefilter.setTime(dateRange); - trackUiEvent('app_date_change'); - } else { - trackUiEvent('app_query_change'); + getAllIndexPatterns( + filterableIndexPatterns, + data.indexPatterns, + notifications + ).then((indexPatterns) => { + if (indexPatterns) { + setState((s) => ({ ...s, indexPatternsForTopNav: indexPatterns })); + } + }); } - - setState((s) => ({ - ...s, - dateRange: { - fromDate: dateRange.from, - toDate: dateRange.to, - }, - query: query || s.query, - })); - }} - appName={'lens'} - indexPatterns={state.indexPatternsForTopNav} - showSearchBar={true} - showDatePicker={true} - showQueryBar={true} - showFilterBar={true} - showSaveQuery={core.application.capabilities.visualize.saveQuery as boolean} - savedQuery={state.savedQuery} - onSaved={(savedQuery) => { - setState((s) => ({ ...s, savedQuery })); - }} - onSavedQueryUpdated={(savedQuery) => { - const savedQueryFilters = savedQuery.attributes.filters || []; - const globalFilters = data.query.filterManager.getGlobalFilters(); - data.query.filterManager.setFilters([...globalFilters, ...savedQueryFilters]); - setState((s) => ({ - ...s, - savedQuery: { ...savedQuery }, // Shallow query for reference issues - dateRange: savedQuery.attributes.timefilter - ? { - fromDate: savedQuery.attributes.timefilter.from, - toDate: savedQuery.attributes.timefilter.to, - } - : s.dateRange, - })); - }} - onClearSavedQuery={() => { - data.query.filterManager.setFilters(data.query.filterManager.getGlobalFilters()); - setState((s) => ({ - ...s, - savedQuery: undefined, - filters: data.query.filterManager.getGlobalFilters(), - query: data.query.queryString.getDefaultQuery(), - })); - }} - query={state.query} - dateRangeFrom={state.dateRange.fromDate} - dateRangeTo={state.dateRange.toDate} - indicateNoData={state.indicateNoData} - /> -
- - {(!state.isLoading || state.persistedDoc) && ( - { - if (isSaveable !== state.isSaveable) { - setState((s) => ({ ...s, isSaveable })); - } - if (!_.isEqual(state.persistedDoc, doc)) { - setState((s) => ({ ...s, lastKnownDoc: doc })); - } - - // Update the cached index patterns if the user made a change to any of them - if ( - state.indexPatternsForTopNav.length !== filterableIndexPatterns.length || - filterableIndexPatterns.some( - (id) => - !state.indexPatternsForTopNav.find((indexPattern) => indexPattern.id === id) - ) - ) { - getAllIndexPatterns( - filterableIndexPatterns, - data.indexPatterns, - core.notifications - ).then((indexPatterns) => { - if (indexPatterns) { - setState((s) => ({ ...s, indexPatternsForTopNav: indexPatterns })); - } - }); - } - }, - }} - /> - )} -
- {lastKnownDoc && state.isSaveModalVisible && ( - runSave(props)} - onClose={() => setState((s) => ({ ...s, isSaveModalVisible: false }))} - getAppNameFromId={getAppNameFromId} - documentInfo={{ - id: lastKnownDoc.id, - title: lastKnownDoc.title || '', - description: lastKnownDoc.description || '', + }, }} - objectType={i18n.translate('xpack.lens.app.saveModalType', { - defaultMessage: 'Lens visualization', - })} - data-test-subj="lnsApp_saveModalOrigin" /> )} - - +
+ {lastKnownDoc && state.isSaveModalVisible && ( + runSave(props, { saveToLibrary: true })} + onClose={() => { + setState((s) => ({ ...s, isSaveModalVisible: false })); + }} + getAppNameFromId={() => getOriginatingAppName()} + documentInfo={{ + id: lastKnownDoc.savedObjectId, + title: lastKnownDoc.title || '', + description: lastKnownDoc.description || '', + }} + returnToOriginSwitchLabel={ + getIsByValueMode() && initialInput + ? i18n.translate('xpack.lens.app.updatePanel', { + defaultMessage: 'Update panel on {originatingAppName}', + values: { originatingAppName: getOriginatingAppName() }, + }) + : undefined + } + objectType={i18n.translate('xpack.lens.app.saveModalType', { + defaultMessage: 'Lens visualization', + })} + data-test-subj="lnsApp_saveModalOrigin" + /> + )} + ); } diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx new file mode 100644 index 0000000000000..f6234d063d8cd --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; +import { LensTopNavActions } from './types'; + +export function getLensTopNavConfig(options: { + showSaveAndReturn: boolean; + showCancel: boolean; + isByValueMode: boolean; + actions: LensTopNavActions; + savingPermitted: boolean; +}): TopNavMenuData[] { + const { showSaveAndReturn, showCancel, actions, savingPermitted } = options; + const topNavMenu: TopNavMenuData[] = []; + + const saveButtonLabel = options.isByValueMode + ? i18n.translate('xpack.lens.app.addToLibrary', { + defaultMessage: 'Save to library', + }) + : options.showSaveAndReturn + ? i18n.translate('xpack.lens.app.saveAs', { + defaultMessage: 'Save as', + }) + : i18n.translate('xpack.lens.app.save', { + defaultMessage: 'Save', + }); + + if (showSaveAndReturn) { + topNavMenu.push({ + label: i18n.translate('xpack.lens.app.saveAndReturn', { + defaultMessage: 'Save and return', + }), + emphasize: true, + iconType: 'check', + run: actions.saveAndReturn, + testId: 'lnsApp_saveAndReturnButton', + disableButton: !savingPermitted, + description: i18n.translate('xpack.lens.app.saveAndReturnButtonAriaLabel', { + defaultMessage: 'Save the current lens visualization and return to the last app', + }), + }); + } + + topNavMenu.push({ + label: saveButtonLabel, + emphasize: !showSaveAndReturn, + run: actions.showSaveModal, + testId: 'lnsApp_saveButton', + description: i18n.translate('xpack.lens.app.saveButtonAriaLabel', { + defaultMessage: 'Save the current lens visualization', + }), + disableButton: !savingPermitted, + }); + + if (showCancel) { + topNavMenu.push({ + label: i18n.translate('xpack.lens.app.cancel', { + defaultMessage: 'cancel', + }), + run: actions.cancel, + testId: 'lnsApp_cancelButton', + description: i18n.translate('xpack.lens.app.cancelButtonAriaLabel', { + defaultMessage: 'Return to the last app without saving changes', + }), + }); + } + return topNavMenu; +} diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index ebc38e4929f6c..0d50e541d3e48 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -11,6 +11,7 @@ import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom import { render, unmountComponentAtNode } from 'react-dom'; import { i18n } from '@kbn/i18n'; +import { DashboardFeatureFlagConfig } from 'src/plugins/dashboard/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { LensReportManager, setReportManager, trackUiEvent } from '../lens_ui_telemetry'; @@ -18,76 +19,116 @@ import { LensReportManager, setReportManager, trackUiEvent } from '../lens_ui_te import { App } from './app'; import { EditorFrameStart } from '../types'; import { addHelpMenuToAppChrome } from '../help_menu_util'; -import { SavedObjectIndexStore } from '../persistence'; import { LensPluginStartDependencies } from '../plugin'; -import { LENS_EMBEDDABLE_TYPE } from '../../common'; +import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE } from '../../common'; +import { + LensEmbeddableInput, + LensByReferenceInput, + LensByValueInput, +} from '../editor_frame_service/embeddable/embeddable'; +import { LensAttributeService } from '../lens_attribute_service'; +import { LensAppServices, RedirectToOriginProps } from './types'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; export async function mountApp( core: CoreSetup, params: AppMountParameters, - createEditorFrame: EditorFrameStart['createInstance'] + mountProps: { + createEditorFrame: EditorFrameStart['createInstance']; + getByValueFeatureFlag: () => Promise; + attributeService: LensAttributeService; + } ) { + const { createEditorFrame, getByValueFeatureFlag, attributeService } = mountProps; const [coreStart, startDependencies] = await core.getStartServices(); - const { data: dataStart, navigation, embeddable } = startDependencies; - const savedObjectsClient = coreStart.savedObjects.client; - addHelpMenuToAppChrome(coreStart.chrome, coreStart.docLinks); + const { data, navigation, embeddable } = startDependencies; + + const instance = await createEditorFrame(); + const storage = new Storage(localStorage); + const stateTransfer = embeddable?.getStateTransfer(params.history); + const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(); + + const lensServices: LensAppServices = { + data, + storage, + navigation, + attributeService, + http: coreStart.http, + chrome: coreStart.chrome, + overlays: coreStart.overlays, + uiSettings: coreStart.uiSettings, + application: coreStart.application, + notifications: coreStart.notifications, + savedObjectsClient: coreStart.savedObjects.client, + getOriginatingAppName: () => { + return embeddableEditorIncomingState?.originatingApp + ? stateTransfer?.getAppNameFromId(embeddableEditorIncomingState.originatingApp) + : undefined; + }, + // Temporarily required until the 'by value' paradigm is default. + dashboardFeatureFlag: await getByValueFeatureFlag(), + }; + + addHelpMenuToAppChrome(coreStart.chrome, coreStart.docLinks); coreStart.chrome.docTitle.change( i18n.translate('xpack.lens.pageTitle', { defaultMessage: 'Lens' }) ); - const stateTransfer = embeddable?.getStateTransfer(params.history); - const { originatingApp } = - stateTransfer?.getIncomingEditorState({ keysToRemoveAfterFetch: ['originatingApp'] }) || {}; - - const instance = await createEditorFrame(); - setReportManager( new LensReportManager({ - storage: new Storage(localStorage), http: core.http, + storage, }) ); - const redirectTo = ( - routeProps: RouteComponentProps<{ id?: string }>, - id?: string, - returnToOrigin?: boolean, - newlyCreated?: boolean - ) => { - if (!id) { + + const getInitialInput = ( + routeProps: RouteComponentProps<{ id?: string }> + ): LensEmbeddableInput | undefined => { + if (routeProps.match.params.id) { + return { savedObjectId: routeProps.match.params.id } as LensByReferenceInput; + } + if (embeddableEditorIncomingState?.valueInput) { + return embeddableEditorIncomingState?.valueInput as LensByValueInput; + } + }; + + const redirectTo = (routeProps: RouteComponentProps<{ id?: string }>, savedObjectId?: string) => { + if (!savedObjectId) { routeProps.history.push('/'); - } else if (!originatingApp) { - routeProps.history.push(`/edit/${id}`); - } else if (!!originatingApp && id && returnToOrigin) { - routeProps.history.push(`/edit/${id}`); - - if (newlyCreated && stateTransfer) { - stateTransfer.navigateToWithEmbeddablePackage(originatingApp, { - state: { id, type: LENS_EMBEDDABLE_TYPE }, - }); - } else { - coreStart.application.navigateToApp(originatingApp); - } + } else { + routeProps.history.push(`/edit/${savedObjectId}`); } }; + const redirectToOrigin = (props?: RedirectToOriginProps) => { + if (!embeddableEditorIncomingState?.originatingApp) { + throw new Error('redirectToOrigin called without an originating app'); + } + if (stateTransfer && props?.input) { + const { input, isCopied } = props; + stateTransfer.navigateToWithEmbeddablePackage(embeddableEditorIncomingState?.originatingApp, { + state: { + embeddableId: isCopied ? undefined : embeddableEditorIncomingState.embeddableId, + type: LENS_EMBEDDABLE_TYPE, + input, + }, + }); + } else { + coreStart.application.navigateToApp(embeddableEditorIncomingState?.originatingApp); + } + }; + + // const featureFlagConfig = await getByValueFeatureFlag(); const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => { trackUiEvent('loaded'); - return ( - redirectTo(routeProps, id, returnToOrigin, newlyCreated) - } - originatingApp={originatingApp} - getAppNameFromId={stateTransfer.getAppNameFromId} + initialInput={getInitialInput(routeProps)} + redirectTo={(savedObjectId?: string) => redirectTo(routeProps, savedObjectId)} + redirectToOrigin={redirectToOrigin} onAppLeave={params.onAppLeave} setHeaderActionMenu={params.setHeaderActionMenu} history={routeProps.history} @@ -103,13 +144,16 @@ export async function mountApp( params.element.classList.add('lnsAppWrapper'); render( - - - - - - - + + + + + + + + + + , params.element ); diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts new file mode 100644 index 0000000000000..fcdd0b20f8d27 --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/types.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { History } from 'history'; +import { + ApplicationStart, + AppMountParameters, + ChromeStart, + HttpStart, + IUiSettingsClient, + NotificationsStart, + OverlayStart, + SavedObjectsStart, +} from '../../../../../src/core/public'; +import { + DataPublicPluginStart, + Filter, + IndexPattern, + Query, + SavedQuery, +} from '../../../../../src/plugins/data/public'; +import { Document } from '../persistence'; +import { LensEmbeddableInput } from '../editor_frame_service/embeddable/embeddable'; +import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public'; +import { LensAttributeService } from '../lens_attribute_service'; +import { IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public'; +import { DashboardFeatureFlagConfig } from '../../../../../src/plugins/dashboard/public'; +import { EmbeddableEditorState } from '../../../../../src/plugins/embeddable/public'; +import { EditorFrameInstance } from '..'; + +export interface LensAppState { + isLoading: boolean; + persistedDoc?: Document; + lastKnownDoc?: Document; + isSaveModalVisible: boolean; + + // Used to show a popover that guides the user towards changing the date range when no data is available. + indicateNoData: boolean; + + // index patterns used to determine which filters are available in the top nav. + indexPatternsForTopNav: IndexPattern[]; + + // Determines whether the lens editor shows the 'save and return' button, and the originating app breadcrumb. + isLinkedToOriginatingApp?: boolean; + + // Properties needed to interface with TopNav + dateRange: { + fromDate: string; + toDate: string; + }; + query: Query; + filters: Filter[]; + savedQuery?: SavedQuery; + isSaveable: boolean; +} + +export interface RedirectToOriginProps { + input?: LensEmbeddableInput; + isCopied?: boolean; +} + +export interface LensAppProps { + history: History; + editorFrame: EditorFrameInstance; + onAppLeave: AppMountParameters['onAppLeave']; + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; + redirectTo: (savedObjectId?: string) => void; + redirectToOrigin?: (props?: RedirectToOriginProps) => void; + + // The initial input passed in by the container when editing. Can be either by reference or by value. + initialInput?: LensEmbeddableInput; + + // State passed in by the container which is used to determine the id of the Originating App. + incomingState?: EmbeddableEditorState; +} + +export interface LensAppServices { + http: HttpStart; + chrome: ChromeStart; + overlays: OverlayStart; + storage: IStorageWrapper; + data: DataPublicPluginStart; + uiSettings: IUiSettingsClient; + application: ApplicationStart; + notifications: NotificationsStart; + navigation: NavigationPublicPluginStart; + attributeService: LensAttributeService; + savedObjectsClient: SavedObjectsStart['client']; + getOriginatingAppName: () => string | undefined; + + // Temporarily required until the 'by value' paradigm is default. + dashboardFeatureFlag: DashboardFeatureFlagConfig; +} + +export interface LensTopNavActions { + saveAndReturn: () => void; + showSaveModal: () => void; + cancel: () => void; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts index 6da6d5a8c118f..4cb523f128a8c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts @@ -59,7 +59,7 @@ export function getSavedObjectFormat({ return { doc: { - id: state.persistedId, + savedObjectId: state.persistedId, title: state.title, description: state.description, type: 'lens', diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts index c7f505aeca517..80d007e17f711 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts @@ -376,7 +376,7 @@ describe('editor_frame state management', () => { { type: 'VISUALIZATION_LOADED', doc: { - id: 'b', + savedObjectId: 'b', state: { datasourceStates: { a: { foo: 'c' } }, visualization: { bar: 'd' }, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts index 09674ebf2ade2..fc8daaed059dd 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts @@ -156,7 +156,7 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta case 'VISUALIZATION_LOADED': return { ...state, - persistedId: action.doc.id, + persistedId: action.doc.savedObjectId, title: action.doc.title, description: action.doc.description, datasourceStates: Object.entries(action.doc.state.datasourceStates).reduce( diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx index 1e2df28cad7b1..d48f9ed713caf 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx @@ -5,12 +5,29 @@ */ import { Subject } from 'rxjs'; -import { Embeddable } from './embeddable'; +import { + Embeddable, + LensByValueInput, + LensByReferenceInput, + LensSavedObjectAttributes, + LensEmbeddableInput, +} from './embeddable'; import { ReactExpressionRendererProps } from 'src/plugins/expressions/public'; -import { Query, TimeRange, Filter, TimefilterContract } from 'src/plugins/data/public'; +import { + Query, + TimeRange, + Filter, + TimefilterContract, + IndexPatternsContract, +} from 'src/plugins/data/public'; import { Document } from '../../persistence'; import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public/embeddable'; +import { coreMock, httpServiceMock } from '../../../../../../src/core/public/mocks'; +import { IBasePath } from '../../../../../../src/core/public'; +import { AttributeService } from '../../../../../../src/plugins/dashboard/public'; +import { Ast } from '@kbn/interpreter/common'; +import { LensAttributeService } from '../../lens_attribute_service'; jest.mock('../../../../../../src/plugins/inspector/public/', () => ({ isAvailable: false, @@ -29,61 +46,95 @@ const savedVis: Document = { visualizationType: '', }; +const attributeServiceMockFromSavedVis = (document: Document): LensAttributeService => { + const core = coreMock.createStart(); + const service = new AttributeService< + LensSavedObjectAttributes, + LensByValueInput, + LensByReferenceInput + >( + 'lens', + jest.fn(), + core.savedObjects.client, + core.overlays, + core.i18n.Context, + core.notifications.toasts + ); + service.unwrapAttributes = jest.fn((input: LensByValueInput | LensByReferenceInput) => { + return Promise.resolve({ ...document } as LensSavedObjectAttributes); + }); + service.wrapAttributes = jest.fn(); + return service; +}; + describe('embeddable', () => { let mountpoint: HTMLDivElement; let expressionRenderer: jest.Mock; let getTrigger: jest.Mock; let trigger: { exec: jest.Mock }; + let basePath: IBasePath; + let attributeService: AttributeService< + LensSavedObjectAttributes, + LensByValueInput, + LensByReferenceInput + >; beforeEach(() => { mountpoint = document.createElement('div'); expressionRenderer = jest.fn((_props) => null); trigger = { exec: jest.fn() }; getTrigger = jest.fn(() => trigger); + attributeService = attributeServiceMockFromSavedVis(savedVis); + const http = httpServiceMock.createSetupContract({ basePath: '/test' }); + basePath = http.basePath; }); afterEach(() => { mountpoint.remove(); }); - it('should render expression with expression renderer', () => { + it('should render expression with expression renderer', async () => { const embeddable = new Embeddable( - dataPluginMock.createSetupContract().query.timefilter.timefilter, - expressionRenderer, - getTrigger, { - editPath: '', - editUrl: '', + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService, + expressionRenderer, + basePath, + indexPatternService: {} as IndexPatternsContract, editable: true, - savedVis, - expression: 'my | expression', + getTrigger, + documentToExpression: () => Promise.resolve({} as Ast), + toExpressionString: () => 'my | expression', }, - { id: '123' } + {} as LensEmbeddableInput ); + await embeddable.initializeSavedVis({} as LensEmbeddableInput); embeddable.render(mountpoint); expect(expressionRenderer).toHaveBeenCalledTimes(1); expect(expressionRenderer.mock.calls[0][0]!.expression).toEqual('my | expression'); }); - it('should re-render if new input is pushed', () => { + it('should re-render if new input is pushed', async () => { const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; const query: Query = { language: 'kquery', query: '' }; const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }]; const embeddable = new Embeddable( - dataPluginMock.createSetupContract().query.timefilter.timefilter, - expressionRenderer, - getTrigger, { - editPath: '', - editUrl: '', + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService, + expressionRenderer, + basePath, + indexPatternService: {} as IndexPatternsContract, editable: true, - savedVis, - expression: 'my | expression', + getTrigger, + documentToExpression: () => Promise.resolve({} as Ast), + toExpressionString: () => 'my | expression', }, - { id: '123' } + { id: '123' } as LensEmbeddableInput ); + await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); embeddable.render(mountpoint); embeddable.updateInput({ @@ -95,61 +146,74 @@ describe('embeddable', () => { expect(expressionRenderer).toHaveBeenCalledTimes(2); }); - it('should pass context to embeddable', () => { + it('should pass context to embeddable', async () => { const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; const query: Query = { language: 'kquery', query: '' }; const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }]; + const input = { savedObjectId: '123', timeRange, query, filters } as LensEmbeddableInput; + const embeddable = new Embeddable( - dataPluginMock.createSetupContract().query.timefilter.timefilter, - expressionRenderer, - getTrigger, { - editPath: '', - editUrl: '', + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService, + expressionRenderer, + basePath, + indexPatternService: {} as IndexPatternsContract, editable: true, - savedVis, - expression: 'my | expression', + getTrigger, + documentToExpression: () => Promise.resolve({} as Ast), + toExpressionString: () => 'my | expression', }, - { id: '123', timeRange, query, filters } + input ); + await embeddable.initializeSavedVis(input); embeddable.render(mountpoint); - expect(expressionRenderer.mock.calls[0][0].searchContext).toEqual({ - timeRange, - query: [query, savedVis.state.query], - filters, - }); + expect(expressionRenderer.mock.calls[0][0].searchContext).toEqual( + expect.objectContaining({ + timeRange, + query: [query, savedVis.state.query], + filters, + }) + ); }); - it('should merge external context with query and filters of the saved object', () => { + it('should merge external context with query and filters of the saved object', async () => { const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; const query: Query = { language: 'kquery', query: 'external filter' }; const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }]; + const newSavedVis = { + ...savedVis, + state: { + ...savedVis.state, + query: { language: 'kquery', query: 'saved filter' }, + filters: [ + { meta: { alias: 'test', negate: false, disabled: false, indexRefName: 'filter-0' } }, + ], + }, + references: [{ type: 'index-pattern', name: 'filter-0', id: 'my-index-pattern-id' }], + }; + attributeService = attributeServiceMockFromSavedVis(newSavedVis); + + const input = { savedObjectId: '123', timeRange, query, filters } as LensEmbeddableInput; + const embeddable = new Embeddable( - dataPluginMock.createSetupContract().query.timefilter.timefilter, - expressionRenderer, - getTrigger, { - editPath: '', - editUrl: '', + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService, + expressionRenderer, + basePath, + indexPatternService: {} as IndexPatternsContract, editable: true, - savedVis: { - ...savedVis, - state: { - ...savedVis.state, - query: { language: 'kquery', query: 'saved filter' }, - filters: [ - { meta: { alias: 'test', negate: false, disabled: false, indexRefName: 'filter-0' } }, - ], - }, - references: [{ type: 'index-pattern', name: 'filter-0', id: 'my-index-pattern-id' }], - }, - expression: 'my | expression', + getTrigger, + documentToExpression: () => Promise.resolve({} as Ast), + toExpressionString: () => 'my | expression', }, - { id: '123', timeRange, query, filters } + input ); + await embeddable.initializeSavedVis(input); embeddable.render(mountpoint); expect(expressionRenderer.mock.calls[0][0].searchContext).toEqual({ @@ -163,20 +227,22 @@ describe('embeddable', () => { }); }); - it('should execute trigger on event from expression renderer', () => { + it('should execute trigger on event from expression renderer', async () => { const embeddable = new Embeddable( - dataPluginMock.createSetupContract().query.timefilter.timefilter, - expressionRenderer, - getTrigger, { - editPath: '', - editUrl: '', + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService, + expressionRenderer, + basePath, + indexPatternService: {} as IndexPatternsContract, editable: true, - savedVis, - expression: 'my | expression', + getTrigger, + documentToExpression: () => Promise.resolve({} as Ast), + toExpressionString: () => 'my | expression', }, - { id: '123' } + { id: '123' } as LensEmbeddableInput ); + await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); embeddable.render(mountpoint); const onEvent = expressionRenderer.mock.calls[0][0].onEvent!; @@ -190,24 +256,31 @@ describe('embeddable', () => { ); }); - it('should not re-render if only change is in disabled filter', () => { + it('should not re-render if only change is in disabled filter', async () => { const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; const query: Query = { language: 'kquery', query: '' }; const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: true } }]; const embeddable = new Embeddable( - dataPluginMock.createSetupContract().query.timefilter.timefilter, - expressionRenderer, - getTrigger, { - editPath: '', - editUrl: '', + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService, + expressionRenderer, + basePath, + indexPatternService: {} as IndexPatternsContract, editable: true, - savedVis, - expression: 'my | expression', + getTrigger, + documentToExpression: () => Promise.resolve({} as Ast), + toExpressionString: () => 'my | expression', }, - { id: '123', timeRange, query, filters } + { id: '123', timeRange, query, filters } as LensEmbeddableInput ); + await embeddable.initializeSavedVis({ + id: '123', + timeRange, + query, + filters, + } as LensEmbeddableInput); embeddable.render(mountpoint); embeddable.updateInput({ @@ -219,7 +292,7 @@ describe('embeddable', () => { expect(expressionRenderer).toHaveBeenCalledTimes(1); }); - it('should re-render on auto refresh fetch observable', () => { + it('should re-render on auto refresh fetch observable', async () => { const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; const query: Query = { language: 'kquery', query: '' }; const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: true } }]; @@ -230,18 +303,25 @@ describe('embeddable', () => { } as unknown) as TimefilterContract; const embeddable = new Embeddable( - timefilter, - expressionRenderer, - getTrigger, { - editPath: '', - editUrl: '', + timefilter, + attributeService, + expressionRenderer, + basePath, + indexPatternService: {} as IndexPatternsContract, editable: true, - savedVis, - expression: 'my | expression', + getTrigger, + documentToExpression: () => Promise.resolve({} as Ast), + toExpressionString: () => 'my | expression', }, - { id: '123', timeRange, query, filters } + { id: '123', timeRange, query, filters } as LensEmbeddableInput ); + await embeddable.initializeSavedVis({ + id: '123', + timeRange, + query, + filters, + } as LensEmbeddableInput); embeddable.render(mountpoint); autoRefreshFetchSubject.next(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index 4df218a3e94e9..61a5d8cacdc4f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -13,10 +13,12 @@ import { Query, TimefilterContract, TimeRange, + IndexPattern, } from 'src/plugins/data/public'; import { ExecutionContextSearch } from 'src/plugins/expressions'; import { Subscription } from 'rxjs'; +import { Ast } from '@kbn/interpreter/common'; import { ExpressionRendererEvent, ReactExpressionRendererType, @@ -28,41 +30,56 @@ import { EmbeddableInput, EmbeddableOutput, IContainer, + SavedObjectEmbeddableInput, + ReferenceOrValueEmbeddable, } from '../../../../../../src/plugins/embeddable/public'; import { DOC_TYPE, Document, injectFilterReferences } from '../../persistence'; import { ExpressionWrapper } from './expression_wrapper'; import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; import { isLensBrushEvent, isLensFilterEvent } from '../../types'; -export interface LensEmbeddableConfiguration { - expression: string | null; - savedVis: Document; - editUrl: string; - editPath: string; - editable: boolean; - indexPatterns?: IIndexPattern[]; -} +import { IndexPatternsContract } from '../../../../../../src/plugins/data/public'; +import { getEditPath } from '../../../common'; +import { IBasePath } from '../../../../../../src/core/public'; +import { LensAttributeService } from '../../lens_attribute_service'; -export interface LensEmbeddableInput extends EmbeddableInput { - timeRange?: TimeRange; - query?: Query; - filters?: Filter[]; -} +export type LensSavedObjectAttributes = Omit; + +export type LensByValueInput = { + attributes: LensSavedObjectAttributes; +} & EmbeddableInput; + +export type LensByReferenceInput = SavedObjectEmbeddableInput & EmbeddableInput; +export type LensEmbeddableInput = LensByValueInput | LensByReferenceInput; export interface LensEmbeddableOutput extends EmbeddableOutput { indexPatterns?: IIndexPattern[]; } -export class Embeddable extends AbstractEmbeddable { +export interface LensEmbeddableDeps { + attributeService: LensAttributeService; + documentToExpression: (doc: Document) => Promise; + toExpressionString: (astObj: Ast, type?: string) => string; + editable: boolean; + indexPatternService: IndexPatternsContract; + expressionRenderer: ReactExpressionRendererType; + timefilter: TimefilterContract; + basePath: IBasePath; + getTrigger?: UiActionsStart['getTrigger'] | undefined; +} + +export class Embeddable + extends AbstractEmbeddable + implements ReferenceOrValueEmbeddable { type = DOC_TYPE; private expressionRenderer: ReactExpressionRendererType; - private getTrigger: UiActionsStart['getTrigger'] | undefined; - private expression: string | null; - private savedVis: Document; + private savedVis: Document | undefined; + private expression: string | undefined | null; private domNode: HTMLElement | Element | undefined; private subscription: Subscription; private autoRefreshFetchSubscription: Subscription; + private isInitialized = false; private externalSearchContext: { timeRange?: TimeRange; @@ -72,50 +89,32 @@ export class Embeddable extends AbstractEmbeddable this.onContainerStateChanged(initialInput)); this.subscription = this.getInput$().subscribe((input) => this.onContainerStateChanged(input)); - this.onContainerStateChanged(initialInput); - this.autoRefreshFetchSubscription = timefilter + this.autoRefreshFetchSubscription = deps.timefilter .getAutoRefreshFetch$() .subscribe(this.reload.bind(this)); } public supportedTriggers() { + if (!this.savedVis) { + return []; + } switch (this.savedVis.visualizationType) { case 'lnsXY': return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; @@ -128,6 +127,22 @@ export class Embeddable extends AbstractEmbeddable !filter.meta.disabled) @@ -144,9 +159,7 @@ export class Embeddable extends AbstractEmbeddable, @@ -173,6 +189,9 @@ export class Embeddable extends AbstractEmbeddable { - if (!this.getTrigger || this.input.disableTriggers) { + if (!this.deps.getTrigger || this.input.disableTriggers) { return; } if (isLensBrushEvent(event)) { - this.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ + this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ data: event.data, embeddable: this, }); } if (isLensFilterEvent(event)) { - this.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ + this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ data: event.data, embeddable: this, }); } }; - destroy() { - super.destroy(); - if (this.domNode) { - unmountComponentAtNode(this.domNode); - } - if (this.subscription) { - this.subscription.unsubscribe(); - } - this.autoRefreshFetchSubscription.unsubscribe(); - } - - reload() { + async reload() { const currentTime = Date.now(); if (this.externalSearchContext.lastReloadRequestTime !== currentTime) { this.externalSearchContext = { @@ -233,4 +241,68 @@ export class Embeddable extends AbstractEmbeddable type === 'index-pattern') + .map(async ({ id }) => { + try { + return await this.deps.indexPatternService.get(id); + } catch (error) { + // Unable to load index pattern, ignore error as the index patterns are only used to + // configure the filter and query bar - there is still a good chance to get the visualization + // to show. + return null; + } + }) + .filter((promise): promise is Promise => Boolean(promise)); + const indexPatterns = await Promise.all(promises); + // passing edit url and index patterns to the output of this embeddable for + // the container to pick them up and use them to configure filter bar and + // config dropdown correctly. + const input = this.getInput(); + const title = input.hidePanelTitles ? '' : input.title || this.savedVis.title; + const savedObjectId = (input as LensByReferenceInput).savedObjectId; + this.updateOutput({ + ...this.getOutput(), + defaultTitle: this.savedVis.title, + title, + editPath: getEditPath(savedObjectId), + editUrl: this.deps.basePath.prepend(`/app/lens${getEditPath(savedObjectId)}`), + indexPatterns, + }); + } + + public inputIsRefType = ( + input: LensByValueInput | LensByReferenceInput + ): input is LensByReferenceInput => { + return this.deps.attributeService.inputIsRefType(input); + }; + + public getInputAsRefType = async (): Promise => { + const input = this.deps.attributeService.getExplicitInputFromEmbeddable(this); + return this.deps.attributeService.getInputAsRefType(input, { + showSaveModal: true, + saveModalTitle: this.getTitle(), + }); + }; + + public getInputAsValueType = async (): Promise => { + const input = this.deps.attributeService.getExplicitInputFromEmbeddable(this); + return this.deps.attributeService.getInputAsValueType(input); + }; + + destroy() { + super.destroy(); + if (this.domNode) { + unmountComponentAtNode(this.domNode); + } + if (this.subscription) { + this.subscription.unsubscribe(); + } + this.autoRefreshFetchSubscription.unsubscribe(); + } } diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index b8f9f8de1d286..8771d1ebaddb1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -4,33 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Capabilities, HttpSetup, SavedObjectsClientContract } from 'kibana/public'; +import { Capabilities, HttpSetup } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { RecursiveReadonly } from '@kbn/utility-types'; import { toExpression, Ast } from '@kbn/interpreter/target/common'; import { IndexPatternsContract, - IndexPattern, TimefilterContract, } from '../../../../../../src/plugins/data/public'; import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; import { EmbeddableFactoryDefinition, - ErrorEmbeddable, - EmbeddableInput, IContainer, } from '../../../../../../src/plugins/embeddable/public'; -import { Embeddable } from './embeddable'; -import { SavedObjectIndexStore, DOC_TYPE } from '../../persistence'; -import { getEditPath } from '../../../common'; +import { Embeddable, LensByReferenceInput, LensEmbeddableInput } from './embeddable'; +import { DOC_TYPE } from '../../persistence'; import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; import { Document } from '../../persistence/saved_object_store'; +import { LensAttributeService } from '../../lens_attribute_service'; -interface StartServices { +export interface LensEmbeddableStartServices { timefilter: TimefilterContract; coreHttp: HttpSetup; + attributeService: LensAttributeService; capabilities: RecursiveReadonly; - savedObjectsClient: SavedObjectsClientContract; expressionRenderer: ReactExpressionRendererType; indexPatternService: IndexPatternsContract; uiActions?: UiActionsStart; @@ -47,7 +44,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { getIconForSavedObject: () => 'lensApp', }; - constructor(private getStartServices: () => Promise) {} + constructor(private getStartServices: () => Promise) {} public isEditable = async () => { const { capabilities } = await this.getStartServices(); @@ -66,59 +63,40 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { createFromSavedObject = async ( savedObjectId: string, - input: Partial & { id: string }, + input: LensEmbeddableInput, parent?: IContainer ) => { + if (!(input as LensByReferenceInput).savedObjectId) { + (input as LensByReferenceInput).savedObjectId = savedObjectId; + } + return this.create(input, parent); + }; + + async create(input: LensEmbeddableInput, parent?: IContainer) { const { - savedObjectsClient, - coreHttp, - indexPatternService, timefilter, expressionRenderer, documentToExpression, uiActions, + coreHttp, + attributeService, + indexPatternService, } = await this.getStartServices(); - const store = new SavedObjectIndexStore(savedObjectsClient); - const savedVis = await store.load(savedObjectId); - - const promises = savedVis.references - .filter(({ type }) => type === 'index-pattern') - .map(async ({ id }) => { - try { - return await indexPatternService.get(id); - } catch (error) { - // Unable to load index pattern, ignore error as the index patterns are only used to - // configure the filter and query bar - there is still a good chance to get the visualization - // to show. - return null; - } - }); - const indexPatterns = ( - await Promise.all(promises) - ).filter((indexPattern: IndexPattern | null): indexPattern is IndexPattern => - Boolean(indexPattern) - ); - - const expression = await documentToExpression(savedVis); return new Embeddable( - timefilter, - expressionRenderer, - uiActions?.getTrigger, { - savedVis, - editPath: getEditPath(savedObjectId), - editUrl: coreHttp.basePath.prepend(`/app/lens${getEditPath(savedObjectId)}`), + attributeService, + indexPatternService, + timefilter, + expressionRenderer, editable: await this.isEditable(), - indexPatterns, - expression: expression ? toExpression(expression) : null, + basePath: coreHttp.basePath, + getTrigger: uiActions?.getTrigger, + documentToExpression, + toExpressionString: toExpression, }, input, parent ); - }; - - async create(input: EmbeddableInput) { - return new ErrorEmbeddable('Lens can only be created from a saved object', input); } } diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx index 7b1d091c1c8fe..c1b6d74bb49c0 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.test.tsx @@ -41,7 +41,8 @@ describe('editor_frame service', () => { (async () => { pluginInstance.setup( coreMock.createSetup() as CoreSetup, - pluginSetupDependencies + pluginSetupDependencies, + jest.fn() ); const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); const instance = await publicAPI.createInstance(); @@ -61,7 +62,8 @@ describe('editor_frame service', () => { it('should not have child nodes after unmount', async () => { pluginInstance.setup( coreMock.createSetup() as CoreSetup, - pluginSetupDependencies + pluginSetupDependencies, + jest.fn() ); const publicAPI = pluginInstance.start(coreMock.createStart(), pluginStartDependencies); const instance = await publicAPI.createInstance(); diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index 5fc347179a032..bebc3e6989902 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -25,10 +25,12 @@ import { Document } from '../persistence/saved_object_store'; import { EditorFrame } from './editor_frame'; import { mergeTables } from './merge_tables'; import { formatColumn } from './format_column'; -import { EmbeddableFactory } from './embeddable/embeddable_factory'; +import { EmbeddableFactory, LensEmbeddableStartServices } from './embeddable/embeddable_factory'; import { getActiveDatasourceIdFromDoc } from './editor_frame/state_management'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; +import { DashboardStart } from '../../../../../src/plugins/dashboard/public'; import { persistedStateToExpression } from './editor_frame/state_helpers'; +import { LensAttributeService } from '../lens_attribute_service'; export interface EditorFrameSetupPlugins { data: DataPublicPluginSetup; @@ -39,6 +41,7 @@ export interface EditorFrameSetupPlugins { export interface EditorFrameStartPlugins { data: DataPublicPluginStart; embeddable?: EmbeddableStart; + dashboard?: DashboardStart; expressions: ExpressionsStart; uiActions?: UiActionsStart; } @@ -78,16 +81,17 @@ export class EditorFrameService { public setup( core: CoreSetup, - plugins: EditorFrameSetupPlugins + plugins: EditorFrameSetupPlugins, + getAttributeService: () => LensAttributeService ): EditorFrameSetup { plugins.expressions.registerFunction(() => mergeTables); plugins.expressions.registerFunction(() => formatColumn); - const getStartServices = async () => { + const getStartServices = async (): Promise => { const [coreStart, deps] = await core.getStartServices(); return { + attributeService: getAttributeService(), capabilities: coreStart.application.capabilities, - savedObjectsClient: coreStart.savedObjects.client, coreHttp: coreStart.http, timefilter: deps.data.query.timefilter.timefilter, expressionRenderer: deps.expressions.ReactExpressionRenderer, diff --git a/x-pack/plugins/lens/public/lens_attribute_service.ts b/x-pack/plugins/lens/public/lens_attribute_service.ts new file mode 100644 index 0000000000000..3c43fd98cceb4 --- /dev/null +++ b/x-pack/plugins/lens/public/lens_attribute_service.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreStart } from '../../../../src/core/public'; +import { LensPluginStartDependencies } from './plugin'; +import { AttributeService } from '../../../../src/plugins/dashboard/public'; +import { + LensSavedObjectAttributes, + LensByValueInput, + LensByReferenceInput, +} from './editor_frame_service/embeddable/embeddable'; +import { SavedObjectIndexStore, DOC_TYPE } from './persistence'; + +export type LensAttributeService = AttributeService< + LensSavedObjectAttributes, + LensByValueInput, + LensByReferenceInput +>; + +export function getLensAttributeService( + core: CoreStart, + startDependencies: LensPluginStartDependencies +): LensAttributeService { + const savedObjectStore = new SavedObjectIndexStore(core.savedObjects.client); + return startDependencies.dashboard.getAttributeService< + LensSavedObjectAttributes, + LensByValueInput, + LensByReferenceInput + >(DOC_TYPE, { + customSaveMethod: async ( + type: string, + attributes: LensSavedObjectAttributes, + savedObjectId?: string + ) => { + const savedDoc = await savedObjectStore.save({ + ...attributes, + savedObjectId, + type: DOC_TYPE, + }); + return { id: savedDoc.savedObjectId }; + }, + customUnwrapMethod: (savedObject) => { + return { + ...savedObject.attributes, + references: savedObject.references, + }; + }, + }); +} diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts index ba7c0ee6ae786..6b6f81aeefed0 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.test.ts @@ -42,7 +42,7 @@ describe('LensStore', () => { }); expect(doc).toEqual({ - id: 'FOO', + savedObjectId: 'FOO', title: 'Hello', description: 'My doc', visualizationType: 'bar', @@ -82,7 +82,7 @@ describe('LensStore', () => { test('updates and returns a visualization document', async () => { const { client, store } = testStore(); const doc = await store.save({ - id: 'Gandalf', + savedObjectId: 'Gandalf', title: 'Even the very wise cannot see all ends.', visualizationType: 'line', references: [], @@ -95,7 +95,7 @@ describe('LensStore', () => { }); expect(doc).toEqual({ - id: 'Gandalf', + savedObjectId: 'Gandalf', title: 'Even the very wise cannot see all ends.', visualizationType: 'line', references: [], diff --git a/x-pack/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/plugins/lens/public/persistence/saved_object_store.ts index e4609213ec792..c6b3fd2cc0f65 100644 --- a/x-pack/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/plugins/lens/public/persistence/saved_object_store.ts @@ -13,7 +13,7 @@ import { Query } from '../../../../../src/plugins/data/public'; import { PersistableFilter } from '../../common'; export interface Document { - id?: string; + savedObjectId?: string; type?: string; visualizationType: string | null; title: string; @@ -30,11 +30,11 @@ export interface Document { export const DOC_TYPE = 'lens'; export interface DocumentSaver { - save: (vis: Document) => Promise<{ id: string }>; + save: (vis: Document) => Promise<{ savedObjectId: string }>; } export interface DocumentLoader { - load: (id: string) => Promise; + load: (savedObjectId: string) => Promise; } export type SavedObjectStore = DocumentLoader & DocumentSaver; @@ -46,20 +46,20 @@ export class SavedObjectIndexStore implements SavedObjectStore { this.client = client; } - async save(vis: Document) { - const { id, type, references, ...rest } = vis; + save = async (vis: Document) => { + const { savedObjectId, type, references, ...rest } = vis; // TODO: SavedObjectAttributes should support this kind of object, // remove this workaround when SavedObjectAttributes is updated. const attributes = (rest as unknown) as SavedObjectAttributes; - const result = await (id - ? this.safeUpdate(id, attributes, references) + const result = await (savedObjectId + ? this.safeUpdate(savedObjectId, attributes, references) : this.client.create(DOC_TYPE, attributes, { references, })); - return { ...vis, id: result.id }; - } + return { ...vis, savedObjectId: result.id }; + }; // As Lens is using an object to store its attributes, using the update API // will merge the new attribute object with the old one, not overwriting deleted @@ -68,7 +68,7 @@ export class SavedObjectIndexStore implements SavedObjectStore { // This function fixes this by doing two updates - one to empty out the document setting // every key to null, and a second one to load the new content. private async safeUpdate( - id: string, + savedObjectId: string, attributes: SavedObjectAttributes, references: SavedObjectReference[] ) { @@ -78,14 +78,14 @@ export class SavedObjectIndexStore implements SavedObjectStore { }); return ( await this.client.bulkUpdate([ - { type: DOC_TYPE, id, attributes: resetAttributes, references }, - { type: DOC_TYPE, id, attributes, references }, + { type: DOC_TYPE, id: savedObjectId, attributes: resetAttributes, references }, + { type: DOC_TYPE, id: savedObjectId, attributes, references }, ]) ).savedObjects[1]; } - async load(id: string): Promise { - const { type, attributes, references, error } = await this.client.get(DOC_TYPE, id); + async load(savedObjectId: string): Promise { + const { type, attributes, references, error } = await this.client.get(DOC_TYPE, savedObjectId); if (error) { throw error; @@ -94,7 +94,7 @@ export class SavedObjectIndexStore implements SavedObjectStore { return { ...(attributes as SavedObjectAttributes), references, - id, + savedObjectId, type, } as Document; } diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 60c7011d55300..1655a571721f5 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -7,6 +7,7 @@ import { AppMountParameters, CoreSetup, CoreStart } from 'kibana/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; +import { DashboardStart } from 'src/plugins/dashboard/public'; import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/public'; import { VisualizationsSetup } from 'src/plugins/visualizations/public'; import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; @@ -35,6 +36,7 @@ import { getLensAliasConfig } from './vis_type_alias'; import { getSearchProvider } from './search_provider'; import './index.scss'; +import { getLensAttributeService, LensAttributeService } from './lens_attribute_service'; export interface LensPluginSetupDependencies { urlForwarding: UrlForwardingSetup; @@ -51,13 +53,14 @@ export interface LensPluginStartDependencies { expressions: ExpressionsStart; navigation: NavigationPublicPluginStart; uiActions: UiActionsStart; - embeddable: EmbeddableStart; + dashboard: DashboardStart; + embeddable?: EmbeddableStart; } - export class LensPlugin { private datatableVisualization: DatatableVisualization; private editorFrameService: EditorFrameService; private createEditorFrame: EditorFrameStart['createInstance'] | null = null; + private attributeService: LensAttributeService | null = null; private indexpatternDatasource: IndexPatternDatasource; private xyVisualization: XyVisualization; private metricVisualization: MetricVisualization; @@ -84,11 +87,15 @@ export class LensPlugin { globalSearch, }: LensPluginSetupDependencies ) { - const editorFrameSetupInterface = this.editorFrameService.setup(core, { - data, - embeddable, - expressions, - }); + const editorFrameSetupInterface = this.editorFrameService.setup( + core, + { + data, + embeddable, + expressions, + }, + () => this.attributeService! + ); const dependencies: IndexPatternDatasourceSetupPlugins & XyVisualizationPluginSetupPlugins & DatatableVisualizationPluginSetupPlugins & @@ -110,13 +117,22 @@ export class LensPlugin { visualizations.registerAlias(getLensAliasConfig()); + const getByValueFeatureFlag = async () => { + const [, deps] = await core.getStartServices(); + return deps.dashboard.dashboardFeatureFlagConfig; + }; + core.application.register({ id: 'lens', title: NOT_INTERNATIONALIZED_PRODUCT_NAME, navLinkStatus: AppNavLinkStatus.hidden, mount: async (params: AppMountParameters) => { const { mountApp } = await import('./app_plugin/mounter'); - return mountApp(core, params, this.createEditorFrame!); + return mountApp(core, params, { + createEditorFrame: this.createEditorFrame!, + attributeService: this.attributeService!, + getByValueFeatureFlag, + }); }, }); @@ -138,6 +154,7 @@ export class LensPlugin { } start(core: CoreStart, startDependencies: LensPluginStartDependencies) { + this.attributeService = getLensAttributeService(core, startDependencies); this.createEditorFrame = this.editorFrameService.start(core, startDependencies).createInstance; } diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx index 47f41f2b76f3e..8a0eb8db4d7aa 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx @@ -67,17 +67,17 @@ export function getTopNavConfig({ savedMap.description = newDescription; savedMap.copyOnSave = newCopyOnSave; - let id; + let savedObjectId; try { savedMap.syncWithStore(); - id = await savedMap.save({ + savedObjectId = await savedMap.save({ confirmOverwrite: false, isTitleDuplicateConfirmed, onTitleDuplicate, }); // id not returned when save fails because of duplicate title check. // return and let user confirm duplicate title. - if (!id) { + if (!savedObjectId) { return {}; } } catch (err) { @@ -105,7 +105,7 @@ export function getTopNavConfig({ getCoreChrome().docTitle.change(savedMap.title); setBreadcrumbs(); - goToSpecifiedPath(`/map/${id}${window.location.hash}`); + goToSpecifiedPath(`/map/${savedObjectId}${window.location.hash}`); const newlyCreated = newCopyOnSave || isNewMap; if (newlyCreated && !returnToOrigin) { @@ -113,14 +113,14 @@ export function getTopNavConfig({ } else if (!!originatingApp && returnToOrigin) { if (newlyCreated && stateTransfer) { stateTransfer.navigateToWithEmbeddablePackage(originatingApp, { - state: { id, type: MAP_SAVED_OBJECT_TYPE }, + state: { input: { savedObjectId }, type: MAP_SAVED_OBJECT_TYPE }, }); } else { getNavigateToApp()(originatingApp); } } - return { id }; + return { id: savedObjectId }; } if (hasSaveAndReturnConfig) { From 298d5c1c9f86a4244f323b957bc370c04bbbfac7 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Wed, 23 Sep 2020 15:23:20 -0700 Subject: [PATCH 70/92] [DOCS] Removes duplicate entry from settings doc (#78343) --- docs/setup/settings.asciidoc | 63 +++++++++++++++++------------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 7f48f21db7197..af68f3e541628 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -54,7 +54,7 @@ overwritten by client-side headers, regardless of the |[[elasticsearch-hosts]] `elasticsearch.hosts:` | The URLs of the {es} instances to use for all your queries. All nodes listed here must be on the same cluster. *Default: `[ "http://localhost:9200" ]`* -+ + To enable SSL/TLS for outbound connections to {es}, use the `https` protocol in this setting. @@ -129,7 +129,7 @@ be set to `"required"` or `"optional"` to request a client certificate from [NOTE] ============ -These settings cannot be used in conjunction with +These settings cannot be used in conjunction with <>. ============ @@ -141,9 +141,9 @@ These settings cannot be used in conjunction with certificates, which make up a trusted certificate chain for {es}. This chain is used by {kib} to establish trust when making outbound SSL/TLS connections to {es}. -+ + In addition to this setting, trusted certificates may be specified via -<> and/or +<> and/or <>. | `elasticsearch.ssl.keyPassphrase:` @@ -157,7 +157,7 @@ corresponding private key. These are used by {kib} to authenticate itself when making outbound SSL/TLS connections to {es}. For this setting, you must also set the `xpack.security.http.ssl.client_authentication` setting in {es} to `"required"` or `"optional"` to request a client certificate from {kib}. -+ + If the keystore contains any additional certificates, they are used as a trusted certificate chain for {es}. This chain is used by {kib} to establish trust when making outbound SSL/TLS connections to {es}. In addition to this @@ -178,7 +178,7 @@ This setting cannot be used in conjunction with | `elasticsearch.ssl.keystore.password:` | The password that decrypts the keystore specified via -<>. If the keystore has no password, leave this +<>. If the keystore has no password, leave this as blank. If the keystore has an empty password, set this to `""`. @@ -187,14 +187,14 @@ as blank. If the keystore has an empty password, set this to authority (CA) certificates, which make up a trusted certificate chain for {es}. This chain is used by {kib} to establish trust when making outbound SSL/TLS connections to {es}. -+ + In addition to this setting, trusted certificates may be specified via <> and/or <>. |`elasticsearch.ssl.truststore.password:` | The password that decrypts the trust store specified via -<>. If the trust store +<>. If the trust store has no password, leave this as blank. If the trust store has an empty password, set this to `""`. | `elasticsearch.ssl.verificationMode:` @@ -300,15 +300,16 @@ the `polling` method could be used enabling that option. *Default: `false`* suppress all logging output. *Default: `false`* | `logging.timezone` - | Set to the canonical timezone ID -(for example, `America/Los_Angeles`) to log events using that timezone. For a -list of timezones, refer to https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. *Default: `UTC`* + | Set to the canonical time zone ID +(for example, `America/Los_Angeles`) to log events using that time zone. +For possible values, refer to +https://en.wikipedia.org/wiki/List_of_tz_database_time_zones[database time zones]. *Default: `UTC`* -| [[logging-verbose]] `logging.verbose:` {ece-icon} +| [[logging-verbose]] `logging.verbose:` {ess-icon} | Set to `true` to log all events, including system usage information and all requests. *Default: `false`* -| `map.includeElasticMapsService:` {ess-icon} +| [[regionmap-ES-map]] `map.includeElasticMapsService:` {ess-icon} | Set to `false` to disable connections to Elastic Maps Service. When `includeElasticMapsService` is turned off, only the vector layers configured by <> and the tile layer configured by <> are available in <>. *Default: `true`* @@ -317,7 +318,7 @@ and the tile layer configured by <> are availabl | Set to `true` to proxy all <> Elastic Maps Service requests through the {kib} server. *Default: `false`* -| [[regionmap-settings]] `map.regionmap:` {ess-icon} {ece-icon} +| [[regionmap-settings]] `map.regionmap:` {ess-icon} | Specifies additional vector layers for use in <> visualizations. Each layer object points to an external vector file that contains a geojson @@ -347,16 +348,10 @@ map.regionmap: [cols="2*<"] |=== -| [[regionmap-ES-map]] `map.includeElasticMapsService:` {ece-icon} - | Turns on or off whether layers from the Elastic Maps Service should be included in the vector -layer option list. By turning this off, -only the layers that are configured here will be included. The default is `true`. -This also affects whether tile-service from the Elastic Maps Service will be available. - -| [[regionmap-attribution]] `map.regionmap.layers[].attribution:` {ess-icon} {ece-icon} +| [[regionmap-attribution]] `map.regionmap.layers[].attribution:` {ess-icon} | Optional. References the originating source of the geojson file. -| [[regionmap-fields]] `map.regionmap.layers[].fields[]:` {ess-icon} {ece-icon} +| [[regionmap-fields]] `map.regionmap.layers[].fields[]:` {ess-icon} | Mandatory. Each layer can contain multiple fields to indicate what properties from the geojson features you wish to expose. The following shows how to define multiple @@ -382,11 +377,11 @@ map.regionmap: [cols="2*<"] |=== -| [[regionmap-field-description]] `map.regionmap.layers[].fields[].description:` {ess-icon} {ece-icon} +| [[regionmap-field-description]] `map.regionmap.layers[].fields[].description:` {ess-icon} | Mandatory. The human readable text that is shown under the Options tab when building the Region Map visualization. -| [[regionmap-field-name]] `map.regionmap.layers[].fields[].name:` {ess-icon} {ece-icon} +| [[regionmap-field-name]] `map.regionmap.layers[].fields[].name:` {ess-icon} | Mandatory. This value is used to do an inner-join between the document stored in {es} and the geojson file. For example, if the field in the geojson is @@ -394,30 +389,30 @@ called `Location` and has city names, there must be a field in {es} that holds the same values that {kib} can then use to lookup for the geoshape data. -| [[regionmap-name]] `map.regionmap.layers[].name:` {ess-icon} {ece-icon} +| [[regionmap-name]] `map.regionmap.layers[].name:` {ess-icon} | Mandatory. A description of the map being provided. -| [[regionmap-url]] `map.regionmap.layers[].url:` {ess-icon} {ece-icon} +| [[regionmap-url]] `map.regionmap.layers[].url:` {ess-icon} | Mandatory. The location of the geojson file as provided by a webserver. -| [[tilemap-settings]] `map.tilemap.options.attribution:` {ess-icon} {ece-icon} +| [[tilemap-settings]] `map.tilemap.options.attribution:` {ess-icon} | The map attribution string. *Default: `"© [Elastic Maps Service](https://www.elastic.co/elastic-maps-service)"`* -| [[tilemap-max-zoom]] `map.tilemap.options.maxZoom:` {ess-icon} {ece-icon} +| [[tilemap-max-zoom]] `map.tilemap.options.maxZoom:` {ess-icon} | The maximum zoom level. *Default: `10`* -| [[tilemap-min-zoom]] `map.tilemap.options.minZoom:` {ess-icon} {ece-icon} +| [[tilemap-min-zoom]] `map.tilemap.options.minZoom:` {ess-icon} | The minimum zoom level. *Default: `1`* -| [[tilemap-subdomains]] `map.tilemap.options.subdomains:` {ess-icon} {ece-icon} +| [[tilemap-subdomains]] `map.tilemap.options.subdomains:` {ess-icon} | An array of subdomains used by the tile service. Specify the position of the subdomain the URL with the token `{s}`. -| [[tilemap-url]] `map.tilemap.url:` {ess-icon} {ece-icon} +| [[tilemap-url]] `map.tilemap.url:` {ess-icon} | The URL to the tileservice that {kib} uses to display map tiles in tilemap visualizations. By default, {kib} reads this URL from an external metadata service, but users can @@ -521,7 +516,7 @@ These settings cannot be used in conjunction with <> and/or <>. | `server.ssl.cipherSuites:` @@ -549,7 +544,7 @@ is optional, as the key may not be encrypted. keystore contains any additional certificates, those will be used as a trusted certificate chain for {kib}. All of these are used by {kib} to establish trust when receiving inbound SSL/TLS connections from end users. The certificate chain is also used by {kib} to verify client certificates from end users when PKI authentication is enabled. -+ + In addition to this setting, trusted certificates may be specified via <> and/or <>. @@ -571,7 +566,7 @@ keystore has no password, leave this unset. If the keystore has an empty passwor | Path to a PKCS#12 trust store that contains one or more X.509 certificate authority (CA) certificates which make up a trusted certificate chain for {kib}. This chain is used by {kib} to establish trust when receiving inbound SSL/TLS connections from end users. If PKI authentication is enabled, this chain is also used by {kib} to verify client certificates from end users. -+ + In addition to this setting, trusted certificates may be specified via <> and/or <>. From 8ea5c575eb8cf1dc38649ea330eeddaacf9110fd Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Wed, 23 Sep 2020 16:31:34 -0700 Subject: [PATCH 71/92] [Metrics UI] Reduce the pagination size for snapshot request (#78051) Co-authored-by: Elastic Machine --- .../snapshot/lib/transform_request_to_metrics_api_request.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts index 814ec5e74ff33..ca64d832667a8 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts @@ -34,7 +34,7 @@ export const transformRequestToMetricsAPIRequest = async ( interval: timeRangeWithIntervalApplied.interval, }, metrics: transformSnapshotMetricsToMetricsAPIMetrics(snapshotRequest), - limit: snapshotRequest.overrideCompositeSize ? snapshotRequest.overrideCompositeSize : 10, + limit: snapshotRequest.overrideCompositeSize ? snapshotRequest.overrideCompositeSize : 5, alignDataToEnd: true, }; From ca27ec8385f547a3a0b87d7dc36067388207ceba Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Wed, 23 Sep 2020 16:32:24 -0700 Subject: [PATCH 72/92] [Metrics UI] Fix EC2 Query to only include aws.ec2 nodes (#78236) * [Metrics UI] Fix EC2 Query to only include aws.ec2 nodes * Making the filter more generic so we can apply it easily to any inventory model --- .../plugins/infra/common/inventory_models/aws_ec2/index.ts | 1 + x-pack/plugins/infra/common/inventory_models/types.ts | 1 + .../lib/transform_request_to_metrics_api_request.ts | 7 ++++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/infra/common/inventory_models/aws_ec2/index.ts b/x-pack/plugins/infra/common/inventory_models/aws_ec2/index.ts index c12137f7810d4..6453332be4f50 100644 --- a/x-pack/plugins/infra/common/inventory_models/aws_ec2/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/aws_ec2/index.ts @@ -31,4 +31,5 @@ export const awsEC2: InventoryModel = { }, requiredMetrics: ['awsEC2CpuUtilization', 'awsEC2NetworkTraffic', 'awsEC2DiskIOBytes'], tooltipMetrics: ['cpu', 'rx', 'tx'], + nodeFilter: [{ term: { 'event.dataset': 'aws.ec2' } }], }; diff --git a/x-pack/plugins/infra/common/inventory_models/types.ts b/x-pack/plugins/infra/common/inventory_models/types.ts index 7eb74056dcf28..5cc788f238365 100644 --- a/x-pack/plugins/infra/common/inventory_models/types.ts +++ b/x-pack/plugins/infra/common/inventory_models/types.ts @@ -371,4 +371,5 @@ export interface InventoryModel { metrics: InventoryMetrics; requiredMetrics: InventoryMetric[]; tooltipMetrics: SnapshotMetricType[]; + nodeFilter?: object[]; } diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts index ca64d832667a8..b18b45f4935d2 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { findInventoryFields } from '../../../../common/inventory_models'; +import { findInventoryFields, findInventoryModel } from '../../../../common/inventory_models'; import { MetricsAPIRequest, SnapshotRequest } from '../../../../common/http_api'; import { ESSearchClient } from '../../../lib/metrics/types'; import { InfraSource } from '../../../lib/sources'; @@ -52,6 +52,11 @@ export const transformRequestToMetricsAPIRequest = async ( filters.push({ term: { 'cloud.region': snapshotRequest.region } }); } + const inventoryModel = findInventoryModel(snapshotRequest.nodeType); + if (inventoryModel && inventoryModel.nodeFilter) { + inventoryModel.nodeFilter?.forEach((f) => filters.push(f)); + } + const inventoryFields = findInventoryFields( snapshotRequest.nodeType, source.configuration.fields From 441ebf65f77863de263b702666e9c5bdbe8cd2d1 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Wed, 23 Sep 2020 18:53:21 -0500 Subject: [PATCH 73/92] make QueryStringInput props explicit (#78336) --- .../kibana-plugin-plugins-data-public.md | 1 + ...in-plugins-data-public.querystringinput.md | 2 +- ...querystringinputprops.bubblesubmitevent.md | 11 +++++ ...-public.querystringinputprops.classname.md | 11 +++++ ...blic.querystringinputprops.datatestsubj.md | 11 +++++ ....querystringinputprops.disableautofocus.md | 11 +++++ ...lic.querystringinputprops.indexpatterns.md | 11 +++++ ...-public.querystringinputprops.isinvalid.md | 11 +++++ ...s.languageswitcherpopoveranchorposition.md | 11 +++++ ...ugins-data-public.querystringinputprops.md | 34 ++++++++++++++ ...ata-public.querystringinputprops.onblur.md | 11 +++++ ...a-public.querystringinputprops.onchange.md | 11 +++++ ...tringinputprops.onchangequeryinputfocus.md | 11 +++++ ...a-public.querystringinputprops.onsubmit.md | 11 +++++ ...blic.querystringinputprops.persistedlog.md | 11 +++++ ...ublic.querystringinputprops.placeholder.md | 11 +++++ ...ta-public.querystringinputprops.prepend.md | 11 +++++ ...data-public.querystringinputprops.query.md | 11 +++++ ...ublic.querystringinputprops.screentitle.md | 11 +++++ ...-data-public.querystringinputprops.size.md | 11 +++++ src/plugins/data/public/index.ts | 1 + src/plugins/data/public/public.api.md | 47 ++++++++++++++++++- src/plugins/data/public/ui/index.ts | 2 +- .../query_string_input/query_string_input.tsx | 9 ++-- 24 files changed, 276 insertions(+), 7 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.bubblesubmitevent.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.classname.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.datatestsubj.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.disableautofocus.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.indexpatterns.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.isinvalid.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.languageswitcherpopoveranchorposition.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.onblur.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.onchange.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.onchangequeryinputfocus.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.onsubmit.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.persistedlog.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.placeholder.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.prepend.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.query.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.screentitle.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.size.md diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index accf46f534e89..8625120d54848 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -79,6 +79,7 @@ | [OptionedValueProp](./kibana-plugin-plugins-data-public.optionedvalueprop.md) | | | [QueryState](./kibana-plugin-plugins-data-public.querystate.md) | All query state service state | | [QueryStateChange](./kibana-plugin-plugins-data-public.querystatechange.md) | | +| [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) | | | [QuerySuggestionBasic](./kibana-plugin-plugins-data-public.querysuggestionbasic.md) | \* | | [QuerySuggestionField](./kibana-plugin-plugins-data-public.querysuggestionfield.md) | \* | | [QuerySuggestionGetFnArgs](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.md) | \* | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md index e85747b8cc3d7..aa7c3bb5d4932 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md @@ -7,5 +7,5 @@ Signature: ```typescript -QueryStringInput: React.FC> +QueryStringInput: React.FC ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.bubblesubmitevent.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.bubblesubmitevent.md new file mode 100644 index 0000000000000..5a41852001ac0 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.bubblesubmitevent.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [bubbleSubmitEvent](./kibana-plugin-plugins-data-public.querystringinputprops.bubblesubmitevent.md) + +## QueryStringInputProps.bubbleSubmitEvent property + +Signature: + +```typescript +bubbleSubmitEvent?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.classname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.classname.md new file mode 100644 index 0000000000000..7fa3b76977183 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.classname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [className](./kibana-plugin-plugins-data-public.querystringinputprops.classname.md) + +## QueryStringInputProps.className property + +Signature: + +```typescript +className?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.datatestsubj.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.datatestsubj.md new file mode 100644 index 0000000000000..edaedf49f4b10 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.datatestsubj.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [dataTestSubj](./kibana-plugin-plugins-data-public.querystringinputprops.datatestsubj.md) + +## QueryStringInputProps.dataTestSubj property + +Signature: + +```typescript +dataTestSubj?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.disableautofocus.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.disableautofocus.md new file mode 100644 index 0000000000000..cc4c6f606409e --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.disableautofocus.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [disableAutoFocus](./kibana-plugin-plugins-data-public.querystringinputprops.disableautofocus.md) + +## QueryStringInputProps.disableAutoFocus property + +Signature: + +```typescript +disableAutoFocus?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.indexpatterns.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.indexpatterns.md new file mode 100644 index 0000000000000..3783138696020 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.indexpatterns.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [indexPatterns](./kibana-plugin-plugins-data-public.querystringinputprops.indexpatterns.md) + +## QueryStringInputProps.indexPatterns property + +Signature: + +```typescript +indexPatterns: Array; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.isinvalid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.isinvalid.md new file mode 100644 index 0000000000000..a282ac3bc5049 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.isinvalid.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [isInvalid](./kibana-plugin-plugins-data-public.querystringinputprops.isinvalid.md) + +## QueryStringInputProps.isInvalid property + +Signature: + +```typescript +isInvalid?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.languageswitcherpopoveranchorposition.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.languageswitcherpopoveranchorposition.md new file mode 100644 index 0000000000000..d133a0930b53d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.languageswitcherpopoveranchorposition.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [languageSwitcherPopoverAnchorPosition](./kibana-plugin-plugins-data-public.querystringinputprops.languageswitcherpopoveranchorposition.md) + +## QueryStringInputProps.languageSwitcherPopoverAnchorPosition property + +Signature: + +```typescript +languageSwitcherPopoverAnchorPosition?: PopoverAnchorPosition; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.md new file mode 100644 index 0000000000000..d503980da7947 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.md @@ -0,0 +1,34 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) + +## QueryStringInputProps interface + +Signature: + +```typescript +export interface QueryStringInputProps +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [bubbleSubmitEvent](./kibana-plugin-plugins-data-public.querystringinputprops.bubblesubmitevent.md) | boolean | | +| [className](./kibana-plugin-plugins-data-public.querystringinputprops.classname.md) | string | | +| [dataTestSubj](./kibana-plugin-plugins-data-public.querystringinputprops.datatestsubj.md) | string | | +| [disableAutoFocus](./kibana-plugin-plugins-data-public.querystringinputprops.disableautofocus.md) | boolean | | +| [indexPatterns](./kibana-plugin-plugins-data-public.querystringinputprops.indexpatterns.md) | Array<IIndexPattern | string> | | +| [isInvalid](./kibana-plugin-plugins-data-public.querystringinputprops.isinvalid.md) | boolean | | +| [languageSwitcherPopoverAnchorPosition](./kibana-plugin-plugins-data-public.querystringinputprops.languageswitcherpopoveranchorposition.md) | PopoverAnchorPosition | | +| [onBlur](./kibana-plugin-plugins-data-public.querystringinputprops.onblur.md) | () => void | | +| [onChange](./kibana-plugin-plugins-data-public.querystringinputprops.onchange.md) | (query: Query) => void | | +| [onChangeQueryInputFocus](./kibana-plugin-plugins-data-public.querystringinputprops.onchangequeryinputfocus.md) | (isFocused: boolean) => void | | +| [onSubmit](./kibana-plugin-plugins-data-public.querystringinputprops.onsubmit.md) | (query: Query) => void | | +| [persistedLog](./kibana-plugin-plugins-data-public.querystringinputprops.persistedlog.md) | PersistedLog | | +| [placeholder](./kibana-plugin-plugins-data-public.querystringinputprops.placeholder.md) | string | | +| [prepend](./kibana-plugin-plugins-data-public.querystringinputprops.prepend.md) | any | | +| [query](./kibana-plugin-plugins-data-public.querystringinputprops.query.md) | Query | | +| [screenTitle](./kibana-plugin-plugins-data-public.querystringinputprops.screentitle.md) | string | | +| [size](./kibana-plugin-plugins-data-public.querystringinputprops.size.md) | SuggestionsListSize | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.onblur.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.onblur.md new file mode 100644 index 0000000000000..10f2ae2ea4f14 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.onblur.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [onBlur](./kibana-plugin-plugins-data-public.querystringinputprops.onblur.md) + +## QueryStringInputProps.onBlur property + +Signature: + +```typescript +onBlur?: () => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.onchange.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.onchange.md new file mode 100644 index 0000000000000..fee44d7afd506 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.onchange.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [onChange](./kibana-plugin-plugins-data-public.querystringinputprops.onchange.md) + +## QueryStringInputProps.onChange property + +Signature: + +```typescript +onChange?: (query: Query) => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.onchangequeryinputfocus.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.onchangequeryinputfocus.md new file mode 100644 index 0000000000000..0421ae9c8bac5 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.onchangequeryinputfocus.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [onChangeQueryInputFocus](./kibana-plugin-plugins-data-public.querystringinputprops.onchangequeryinputfocus.md) + +## QueryStringInputProps.onChangeQueryInputFocus property + +Signature: + +```typescript +onChangeQueryInputFocus?: (isFocused: boolean) => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.onsubmit.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.onsubmit.md new file mode 100644 index 0000000000000..951ec7419485f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.onsubmit.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [onSubmit](./kibana-plugin-plugins-data-public.querystringinputprops.onsubmit.md) + +## QueryStringInputProps.onSubmit property + +Signature: + +```typescript +onSubmit?: (query: Query) => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.persistedlog.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.persistedlog.md new file mode 100644 index 0000000000000..d1a8efb364016 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.persistedlog.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [persistedLog](./kibana-plugin-plugins-data-public.querystringinputprops.persistedlog.md) + +## QueryStringInputProps.persistedLog property + +Signature: + +```typescript +persistedLog?: PersistedLog; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.placeholder.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.placeholder.md new file mode 100644 index 0000000000000..31e41f4d55205 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.placeholder.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [placeholder](./kibana-plugin-plugins-data-public.querystringinputprops.placeholder.md) + +## QueryStringInputProps.placeholder property + +Signature: + +```typescript +placeholder?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.prepend.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.prepend.md new file mode 100644 index 0000000000000..7be882058d3fd --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.prepend.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [prepend](./kibana-plugin-plugins-data-public.querystringinputprops.prepend.md) + +## QueryStringInputProps.prepend property + +Signature: + +```typescript +prepend?: any; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.query.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.query.md new file mode 100644 index 0000000000000..f15f6d082332b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.query.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [query](./kibana-plugin-plugins-data-public.querystringinputprops.query.md) + +## QueryStringInputProps.query property + +Signature: + +```typescript +query: Query; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.screentitle.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.screentitle.md new file mode 100644 index 0000000000000..0c80252d74571 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.screentitle.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [screenTitle](./kibana-plugin-plugins-data-public.querystringinputprops.screentitle.md) + +## QueryStringInputProps.screenTitle property + +Signature: + +```typescript +screenTitle?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.size.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.size.md new file mode 100644 index 0000000000000..6b0e53a23e07b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinputprops.size.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) > [size](./kibana-plugin-plugins-data-public.querystringinputprops.size.md) + +## QueryStringInputProps.size property + +Signature: + +```typescript +size?: SuggestionsListSize; +``` diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 57865f05871a1..f7dceffa9fdbc 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -420,6 +420,7 @@ export { StatefulSearchBarProps, FilterBar, QueryStringInput, + QueryStringInputProps, IndexPatternSelect, } from './ui'; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index ed58ee840a8f8..28dfbf824470c 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1727,11 +1727,54 @@ export interface QueryStateChange extends QueryStateChangePartial { globalFilters?: boolean; } -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "QueryStringInput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const QueryStringInput: React.FC>; +export const QueryStringInput: React.FC; + +// Warning: (ae-missing-release-tag) "QueryStringInputProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface QueryStringInputProps { + // (undocumented) + bubbleSubmitEvent?: boolean; + // (undocumented) + className?: string; + // (undocumented) + dataTestSubj?: string; + // (undocumented) + disableAutoFocus?: boolean; + // (undocumented) + indexPatterns: Array; + // (undocumented) + isInvalid?: boolean; + // (undocumented) + languageSwitcherPopoverAnchorPosition?: PopoverAnchorPosition; + // (undocumented) + onBlur?: () => void; + // (undocumented) + onChange?: (query: Query) => void; + // (undocumented) + onChangeQueryInputFocus?: (isFocused: boolean) => void; + // (undocumented) + onSubmit?: (query: Query) => void; + // Warning: (ae-forgotten-export) The symbol "PersistedLog" needs to be exported by the entry point index.d.ts + // + // (undocumented) + persistedLog?: PersistedLog; + // (undocumented) + placeholder?: string; + // (undocumented) + prepend?: any; + // (undocumented) + query: Query; + // (undocumented) + screenTitle?: string; + // Warning: (ae-forgotten-export) The symbol "SuggestionsListSize" needs to be exported by the entry point index.d.ts + // + // (undocumented) + size?: SuggestionsListSize; +} // @public (undocumented) export type QuerySuggestion = QuerySuggestionBasic | QuerySuggestionField; diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index 35b1bc50ddb1e..299b9d2681578 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -20,7 +20,7 @@ export { SuggestionsComponent } from './typeahead'; export { IndexPatternSelect } from './index_pattern_select'; export { FilterBar } from './filter_bar'; -export { QueryStringInput } from './query_string_input/query_string_input'; +export { QueryStringInput, QueryStringInputProps } from './query_string_input/query_string_input'; export { SearchBar, SearchBarProps, StatefulSearchBarProps } from './search_bar'; // @internal diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 8e1151b387fee..0986ad0668c24 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -46,8 +46,7 @@ import { PersistedLog, getQueryLog, matchPairs, toUser, fromUser } from '../../q import { SuggestionsListSize } from '../typeahead/suggestions_component'; import { SuggestionsComponent } from '..'; -interface Props { - kibana: KibanaReactContextValue; +export interface QueryStringInputProps { indexPatterns: Array; query: Query; disableAutoFocus?: boolean; @@ -67,6 +66,10 @@ interface Props { isInvalid?: boolean; } +interface Props extends QueryStringInputProps { + kibana: KibanaReactContextValue; +} + interface State { isSuggestionsVisible: boolean; index: number | null; @@ -687,4 +690,4 @@ export class QueryStringInputUI extends Component { } } -export const QueryStringInput = withKibana(QueryStringInputUI); +export const QueryStringInput: React.FC = withKibana(QueryStringInputUI); From 34e8a3f139f6dd636345374238cee5e8b1d10351 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Wed, 23 Sep 2020 18:13:15 -0600 Subject: [PATCH 74/92] [Security Solution] Changes rule details default stack-by value (#78357) --- .../pages/detection_engine/rules/details/index.tsx | 7 +++++++ 1 file changed, 7 insertions(+) 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 4816358e06226..ad8ab3ed3a148 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 @@ -88,6 +88,7 @@ import { timelineDefaults } from '../../../../../timelines/store/timeline/defaul import { TimelineModel } from '../../../../../timelines/store/timeline/model'; import { useSourcererScope } from '../../../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../../../common/store/sourcerer/model'; +import { AlertsHistogramOption } from '../../../../components/alerts_histogram_panel/types'; enum RuleDetailTabs { alerts = 'alerts', @@ -345,6 +346,11 @@ export const RuleDetailsPageComponent: FC = ({ return null; } + const defaultRuleStackByOption: AlertsHistogramOption = { + text: 'event.category', + value: 'event.category', + }; + return ( <> {hasIndexWrite != null && !hasIndexWrite && } @@ -480,6 +486,7 @@ export const RuleDetailsPageComponent: FC = ({ signalIndexName={signalIndexName} setQuery={setQuery} stackByOptions={alertsHistogramOptions} + defaultStackByOption={defaultRuleStackByOption} to={to} updateDateRange={updateDateRangeCallback} /> From 66d11bd1c3c2ecdd2aad922be86f916a0d20c48f Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Wed, 23 Sep 2020 20:09:11 -0600 Subject: [PATCH 75/92] Optimize status lookup for plugins that have no custom statuses (#78342) --- src/core/server/status/plugins_status.test.ts | 8 +-- src/core/server/status/plugins_status.ts | 51 +++++++++++++++---- src/core/server/status/status_service.ts | 6 +-- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/src/core/server/status/plugins_status.test.ts b/src/core/server/status/plugins_status.test.ts index a75dc8c283698..176e2414a8d04 100644 --- a/src/core/server/status/plugins_status.test.ts +++ b/src/core/server/status/plugins_status.test.ts @@ -161,13 +161,13 @@ describe('PluginStatusService', () => { }, b: { level: ServiceStatusLevels.degraded, - summary: '[2] services are degraded', + summary: '[savedObjects]: savedObjects degraded', detail: 'See the status page for more information', meta: expect.any(Object), }, c: { level: ServiceStatusLevels.degraded, - summary: '[3] services are degraded', + summary: '[savedObjects]: savedObjects degraded', detail: 'See the status page for more information', meta: expect.any(Object), }, @@ -186,13 +186,13 @@ describe('PluginStatusService', () => { }, b: { level: ServiceStatusLevels.critical, - summary: '[2] services are critical', + summary: '[elasticsearch]: elasticsearch critical', detail: 'See the status page for more information', meta: expect.any(Object), }, c: { level: ServiceStatusLevels.critical, - summary: '[3] services are critical', + summary: '[elasticsearch]: elasticsearch critical', detail: 'See the status page for more information', meta: expect.any(Object), }, diff --git a/src/core/server/status/plugins_status.ts b/src/core/server/status/plugins_status.ts index 113d59b327c11..988f2d9969ccb 100644 --- a/src/core/server/status/plugins_status.ts +++ b/src/core/server/status/plugins_status.ts @@ -33,7 +33,17 @@ interface Deps { export class PluginsStatusService { private readonly pluginStatuses = new Map>(); private readonly update$ = new BehaviorSubject(true); - constructor(private readonly deps: Deps) {} + private readonly defaultInheritedStatus$: Observable; + + constructor(private readonly deps: Deps) { + this.defaultInheritedStatus$ = this.deps.core$.pipe( + map((coreStatus) => { + return getSummaryStatus(Object.entries(coreStatus), { + allAvailableSummary: `All dependencies are available`, + }); + }) + ); + } public set(plugin: PluginName, status$: Observable) { this.pluginStatuses.set(plugin, status$); @@ -57,14 +67,24 @@ export class PluginsStatusService { } public getDerivedStatus$(plugin: PluginName): Observable { - return combineLatest([this.deps.core$, this.getDependenciesStatus$(plugin)]).pipe( - map(([coreStatus, pluginStatuses]) => { - return getSummaryStatus( - [...Object.entries(coreStatus), ...Object.entries(pluginStatuses)], - { - allAvailableSummary: `All dependencies are available`, - } - ); + return this.update$.pipe( + switchMap(() => { + // Only go up the dependency tree if any of this plugin's dependencies have a custom status + // Helps eliminate memory overhead of creating thousands of Observables unnecessarily. + if (this.anyCustomStatuses(plugin)) { + return combineLatest([this.deps.core$, this.getDependenciesStatus$(plugin)]).pipe( + map(([coreStatus, pluginStatuses]) => { + return getSummaryStatus( + [...Object.entries(coreStatus), ...Object.entries(pluginStatuses)], + { + allAvailableSummary: `All dependencies are available`, + } + ); + }) + ); + } else { + return this.defaultInheritedStatus$; + } }) ); } @@ -95,4 +115,17 @@ export class PluginsStatusService { }) ); } + + /** + * Determines whether or not this plugin or any plugin in it's dependency tree have a custom status registered. + */ + private anyCustomStatuses(plugin: PluginName): boolean { + if (this.pluginStatuses.get(plugin)) { + return true; + } + + return this.deps.pluginDependencies + .get(plugin)! + .reduce((acc, depName) => acc || this.anyCustomStatuses(depName), false as boolean); + } } diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts index 9acf93f2f8197..62f226405e81a 100644 --- a/src/core/server/status/status_service.ts +++ b/src/core/server/status/status_service.ts @@ -70,10 +70,10 @@ export class StatusService implements CoreService { const core$ = this.setupCoreStatus({ elasticsearch, savedObjects }); this.pluginsStatus = new PluginsStatusService({ core$, pluginDependencies }); - const overall$: Observable = combineLatest( + const overall$: Observable = combineLatest([ core$, - this.pluginsStatus.getAll$() - ).pipe( + this.pluginsStatus.getAll$(), + ]).pipe( // Prevent many emissions at once from dependency status resolution from making this too noisy debounceTime(500), map(([coreStatus, pluginsStatus]) => { From 1f03ce41adbb43e6d1e4e98045efef3a72e74aa7 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 23 Sep 2020 20:51:07 -0700 Subject: [PATCH 76/92] [ci-metrics] add docs describing the metrics collected (#78363) Co-authored-by: spalger --- .../development-ci-metrics.asciidoc | 65 +++++++++++++++++++ docs/developer/contributing/index.asciidoc | 3 + 2 files changed, 68 insertions(+) create mode 100644 docs/developer/contributing/development-ci-metrics.asciidoc diff --git a/docs/developer/contributing/development-ci-metrics.asciidoc b/docs/developer/contributing/development-ci-metrics.asciidoc new file mode 100644 index 0000000000000..d4d54f1da7b8b --- /dev/null +++ b/docs/developer/contributing/development-ci-metrics.asciidoc @@ -0,0 +1,65 @@ +[[ci-metrics]] +== CI Metrics + +In addition to running our tests, CI collects metrics about the Kibana build. These metrics are sent to an external service to track changes over time, and to provide PR authors insights into the impact of their changes. + + +[[ci-metric-types]] +=== Metric types + + +[[ci-metric-types-bundle-size-metrics]] +==== Bundle size + +These metrics help contributors know how they are impacting the size of the bundles Kibana creates, and help make sure that Kibana loads as fast as possible. + +[[ci-metric-page-load-bundle-size]] `page load bundle size` :: +The size of the entry file produced for each bundle/plugin. This file is always loaded on every page load, so it should be as small as possible. To reduce this metric you can put any code that isn't necessary on every page load behind an https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Dynamic_Imports[`async import()`]. ++ +Code that is shared statically with other plugins will contribute to the `page load bundle size` of that plugin. This includes exports from the `public/index.ts` file and any file referenced by the `extraPublicDirs` manifest property. + +[[ci-metric-async-chunks-size]] `async chunks size` :: +An "async chunk" is created for the files imported by each https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Dynamic_Imports[`async import()`] statement. This metric tracks the sum size of these chunks, in bytes, broken down by plugin/bundle id. You can think of this as the amount of code users will have to download if they access all the components/applications within a bundle. + +[[ci-metric-misc-asset-size]] `miscellaneous assets size` :: +A "miscellaneous asset" is anything that isn't an async chunk or entry chunk, often images. This metric tracks the sum size of these assets, in bytes, broken down by plugin/bundle id. + +[[ci-metric-bundle-module-count]] `@kbn/optimizer bundle module count` :: +The number of separate modules included in each bundle/plugin. This is the best indicator we have for how long a specific bundle will take to be built by the `@kbn/optimizer`, so we report it to help people know when they've imported a module which might include a surprising number of sub-modules. + + +[[ci-metric-types-distributable-size]] +==== Distributable size + +The size of the Kibana distributable is an essential metric as it not only contributes to the time it takes to download, but it also impacts time it takes to extract the archive once downloaded. + +There are several metrics that we don't report on PRs because gzip-compression produces different file sizes even when provided the same input, so this metric would regularly show changes even though PR authors hadn't made any relevant changes. + +All metrics are collected from the `tar.gz` archive produced for the linux platform. + +[[ci-metric-distributable-file-count]] `distributable file count` :: +The number of files included in the default distributable. + +[[ci-metric-oss-distributable-file-count]] `oss distributable file count` :: +The number of files included in the OSS distributable. + +[[ci-metric-distributable-size]] `distributable size` :: +The size, in bytes, of the default distributable. _(not reported on PRs)_ + +[[ci-metric-oss-distributable-size]] `oss distributable size` :: +The size, in bytes, of the OSS distributable. _(not reported on PRs)_ + + +[[ci-metric-types-saved-object-field-counts]] +==== Saved Object field counts + +Elasticsearch limits the number of fields in an index to 1000 by default, and we want to avoid raising that limit. + +[[ci-metric-saved-object-field-count]] `Saved Objects .kibana field count` :: +The number of saved object fields broken down by saved object type. + + +[[ci-metric-adding-new-metrics]] +=== Adding new metrics + +You can report new metrics by using the `CiStatsReporter` class provided by the `@kbn/dev-utils` package. This class is automatically configured on CI and its methods noop when running outside of CI. For more details checkout the {kib-repo}blob/{branch}/packages/kbn-dev-utils/src/ci_stats_reporter[`CiStatsReporter` readme]. \ No newline at end of file diff --git a/docs/developer/contributing/index.asciidoc b/docs/developer/contributing/index.asciidoc index 99ab83bc2f073..ecb37ffe9c97b 100644 --- a/docs/developer/contributing/index.asciidoc +++ b/docs/developer/contributing/index.asciidoc @@ -9,6 +9,7 @@ Read <> to get your environment up and running, the * <> * <> * <> +* <> * <> * <> * <> @@ -78,6 +79,8 @@ include::development-tests.asciidoc[leveloffset=+1] include::interpreting-ci-failures.asciidoc[leveloffset=+1] +include::development-ci-metrics.asciidoc[leveloffset=+1] + include::development-documentation.asciidoc[leveloffset=+1] include::development-pull-request.asciidoc[leveloffset=+1] From 6a04a6410a37ea720bcad49df074a7ed9586ce46 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Wed, 23 Sep 2020 22:55:02 -0500 Subject: [PATCH 77/92] [Enterprise Search] Update Product Selector and add Setup Guide (#78233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add conditional button text - Only shows error connectign if host is set - Removes conditional rendering of cards - Changes the action text from “Launch” to “Setup” * Add setup guide * Extract ProductSelector to component * Update index and add routes * Change setup guide text * Fix imports * Add missing mock * Update x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx Co-authored-by: Jason Stoltzfus * Remove Literals Co-authored-by: Constance * Remove Literals II - The Force Awakens Co-authored-by: Constance * Add back access checks * Remove hard-coded props 🤦🏼‍♂️ * Remove data-test-subj attr * Reafactor access check variables * Remove unused beforeEach Co-authored-by: Constance * Add newline Co-authored-by: Constance * Update image to compressed * Remove unused things * Update to new way of using lodash things 🤷🏽‍♀️ Co-authored-by: Jason Stoltzfus Co-authored-by: Constance --- .../product_card/product_card.test.tsx | 19 +++- .../components/product_card/product_card.tsx | 32 ++++-- .../components/product_selector/index.ts | 7 ++ .../product_selector.test.tsx | 54 ++++++++++ .../product_selector/product_selector.tsx | 97 ++++++++++++++++++ .../setup_guide/assets/getting_started.png | Bin 0 -> 194538 bytes .../components/setup_guide/index.ts | 7 ++ .../setup_guide/setup_guide.test.tsx | 21 ++++ .../components/setup_guide/setup_guide.tsx | 62 +++++++++++ .../enterprise_search/index.test.tsx | 49 +++------ .../applications/enterprise_search/index.tsx | 84 ++++----------- .../applications/enterprise_search/routes.ts | 8 ++ 12 files changed, 330 insertions(+), 110 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/assets/getting_started.png create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search/routes.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx index f651511e61b44..35301af44b413 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx @@ -5,8 +5,9 @@ */ import '../../../__mocks__/kea.mock'; +import '../../../__mocks__/shallow_usecontext.mock'; -import React from 'react'; +import React, { useContext } from 'react'; import { shallow } from 'enzyme'; import { EuiCard } from '@elastic/eui'; @@ -26,6 +27,7 @@ describe('ProductCard', () => { }); it('renders an App Search card', () => { + (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } })); const wrapper = shallow(); const card = wrapper.find(EuiCard).dive().shallow(); @@ -34,13 +36,14 @@ describe('ProductCard', () => { const button = card.find(EuiButton); expect(button.prop('to')).toEqual('/app/enterprise_search/app_search'); - expect(button.prop('data-test-subj')).toEqual('LaunchAppSearchButton'); + expect(button.prop('children')).toEqual('Launch App Search'); button.simulate('click'); expect(sendTelemetry).toHaveBeenCalledWith(expect.objectContaining({ metric: 'app_search' })); }); it('renders a Workplace Search card', () => { + (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } })); const wrapper = shallow(); const card = wrapper.find(EuiCard).dive().shallow(); @@ -49,11 +52,21 @@ describe('ProductCard', () => { const button = card.find(EuiButton); expect(button.prop('to')).toEqual('/app/enterprise_search/workplace_search'); - expect(button.prop('data-test-subj')).toEqual('LaunchWorkplaceSearchButton'); + expect(button.prop('children')).toEqual('Launch Workplace Search'); button.simulate('click'); expect(sendTelemetry).toHaveBeenCalledWith( expect.objectContaining({ metric: 'workplace_search' }) ); }); + + it('renders correct button text when host not present', () => { + (useContext as jest.Mock).mockImplementation(() => ({ config: { host: '' } })); + + const wrapper = shallow(); + const card = wrapper.find(EuiCard).dive().shallow(); + const button = card.find(EuiButton); + + expect(button.prop('children')).toEqual('Setup Workplace Search'); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx index 833a782a32f00..482d68736af01 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useContext } from 'react'; import { useValues } from 'kea'; -import upperFirst from 'lodash/upperFirst'; -import snakeCase from 'lodash/snakeCase'; +import { snakeCase } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiCard, EuiTextColor } from '@elastic/eui'; +import { KibanaContext, IKibanaContext } from '../../../index'; + import { EuiButton } from '../../../shared/react_router_helpers'; import { sendTelemetry } from '../../../shared/telemetry'; import { HttpLogic } from '../../../shared/http'; @@ -30,6 +31,25 @@ interface IProductCard { export const ProductCard: React.FC = ({ product, image }) => { const { http } = useValues(HttpLogic); + const { + config: { host }, + } = useContext(KibanaContext) as IKibanaContext; + + const LAUNCH_BUTTON_TEXT = i18n.translate( + 'xpack.enterpriseSearch.overview.productCard.launchButton', + { + defaultMessage: 'Launch {productName}', + values: { productName: product.NAME }, + } + ); + + const SETUP_BUTTON_TEXT = i18n.translate( + 'xpack.enterpriseSearch.overview.productCard.setupButton', + { + defaultMessage: 'Setup {productName}', + values: { productName: product.NAME }, + } + ); return ( = ({ product, image }) => { metric: snakeCase(product.ID), }) } - data-test-subj={`Launch${upperFirst(product.ID)}Button`} > - {i18n.translate('xpack.enterpriseSearch.overview.productCard.button', { - defaultMessage: `Launch {productName}`, - values: { productName: product.NAME }, - })} + {host ? LAUNCH_BUTTON_TEXT : SETUP_BUTTON_TEXT} } /> diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/index.ts new file mode 100644 index 0000000000000..b67d130cd68f0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ProductSelector } from './product_selector'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx new file mode 100644 index 0000000000000..44efa57db897f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../__mocks__/shallow_usecontext.mock'; + +import React, { useContext } from 'react'; +import { shallow } from 'enzyme'; +import { EuiPage } from '@elastic/eui'; + +import { ProductSelector } from './'; +import { ProductCard } from '../product_card'; + +describe('ProductSelector', () => { + it('renders the overview page and product cards with no host set', () => { + (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); + const wrapper = shallow(); + + expect(wrapper.find(EuiPage).hasClass('enterpriseSearchOverview')).toBe(true); + expect(wrapper.find(ProductCard)).toHaveLength(2); + }); + + describe('access checks when host is set', () => { + beforeEach(() => { + (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } })); + }); + + it('does not render the App Search card if the user does not have access to AS', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(ProductCard)).toHaveLength(1); + expect(wrapper.find(ProductCard).prop('product').ID).toEqual('workplaceSearch'); + }); + + it('does not render the Workplace Search card if the user does not have access to WS', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(ProductCard)).toHaveLength(1); + expect(wrapper.find(ProductCard).prop('product').ID).toEqual('appSearch'); + }); + + it('does not render any cards if the user does not have access', () => { + const wrapper = shallow(); + + expect(wrapper.find(ProductCard)).toHaveLength(0); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx new file mode 100644 index 0000000000000..07b8d4b9926d7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext } from 'react'; + +import { + EuiPage, + EuiPageBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiPageContentBody, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { KibanaContext, IKibanaContext } from '../../../index'; + +import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; + +import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; + +import { ProductCard } from '../product_card'; + +import AppSearchImage from '../../assets/app_search.png'; +import WorkplaceSearchImage from '../../assets/workplace_search.png'; + +interface IProductSelectorProps { + access: { + hasAppSearchAccess?: boolean; + hasWorkplaceSearchAccess?: boolean; + }; +} + +export const ProductSelector: React.FC = ({ access }) => { + const { hasAppSearchAccess, hasWorkplaceSearchAccess } = access; + const { + config: { host }, + } = useContext(KibanaContext) as IKibanaContext; + + const shouldShowAppSearchCard = !host || hasAppSearchAccess; + const shouldShowWorkplaceSearchCard = !host || hasWorkplaceSearchAccess; + + return ( + + + + + + + + +

+ {i18n.translate('xpack.enterpriseSearch.overview.heading', { + defaultMessage: 'Welcome to Elastic Enterprise Search', + })} +

+
+ +

+ {i18n.translate('xpack.enterpriseSearch.overview.subheading', { + defaultMessage: 'Select a product to get started', + })} +

+
+
+
+ + + {shouldShowAppSearchCard && ( + + + + )} + {shouldShowWorkplaceSearchCard && ( + + + + )} + + + +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/assets/getting_started.png b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/assets/getting_started.png new file mode 100644 index 0000000000000000000000000000000000000000..f0fcb432c29e1b5b9f3b66fd58e0a65784798180 GIT binary patch literal 194538 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�ql5g>VD>@gEGXo+_AivS3P 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

;ShZ*E4sz=OqW`L|zxSiAsU=%j9`OP%xh2@$@ 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

  • qpq6nq)jhEg(+11qUe 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�}Irm>* z#L6}X(dI?X!4oh5q!4XvoQ@Kq15l{=)!V1Dj7$RVes0AoKO;Qv&7*$DK_xx_q!3*S z>!*d58$_=A;mcu>WzE~boNuodRQ_ruiARgujzjb?^iX0I5WbE?sg;$JOdWx9CsGu=JI4?lUs&imc>V=|6tmbY zIrQr|kgT~7w=Bka=wJsRz>oIsAO(RKhQjcpX@v|3rS1yiN^v!~^a6rM5ZrhN7oNr2 zY7v|}wEqXWLI{Mspm*+)YMqp%R`v@9l+*tJjA40x8PsB-xc~+*j_FHHR7^1@0YK+$ zvo0mO`Sc$Gg9WM95L^ { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(SetupGuideLayout)).toHaveLength(1); + expect(wrapper.find(SetPageChrome)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx new file mode 100644 index 0000000000000..fcb3b399c75b0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { ENTERPRISE_SEARCH_PLUGIN } from '../../../../../common/constants'; +import { SetupGuide as SetupGuideLayout } from '../../../shared/setup_guide'; +import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; +import GettingStarted from './assets/getting_started.png'; + +export const SetupGuide: React.FC = () => ( + + + + + + {i18n.translate('xpack.enterpriseSearch.enterpriseSearch.setupGuide.videoAlt', + + + +

    + +

    +
    + + +

    + +

    +
    + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx index b2918dac086f6..2c0902163e3d6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import '../__mocks__/shallow_usecontext.mock'; + +import React, { useContext } from 'react'; import { shallow } from 'enzyme'; import { EuiPage } from '@elastic/eui'; @@ -12,54 +14,31 @@ import '../__mocks__/kea.mock'; import { useValues } from 'kea'; import { EnterpriseSearch } from './'; +import { SetupGuide } from './components/setup_guide'; import { ErrorConnecting } from './components/error_connecting'; -import { ProductCard } from './components/product_card'; +import { ProductSelector } from './components/product_selector'; describe('EnterpriseSearch', () => { beforeEach(() => { (useValues as jest.Mock).mockReturnValue({ errorConnecting: false }); + (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: 'localhost' } })); }); - it('renders the overview page and product cards', () => { - const wrapper = shallow( - - ); + it('renders the Setup Guide and Product Selector', () => { + const wrapper = shallow(); - expect(wrapper.find(EuiPage).hasClass('enterpriseSearchOverview')).toBe(true); - expect(wrapper.find(ProductCard)).toHaveLength(2); + expect(wrapper.find(SetupGuide)).toHaveLength(1); + expect(wrapper.find(ProductSelector)).toHaveLength(1); }); - it('renders the error connecting prompt', () => { + it('renders the error connecting prompt when host is not configured', () => { (useValues as jest.Mock).mockReturnValueOnce({ errorConnecting: true }); + (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); + const wrapper = shallow(); expect(wrapper.find(ErrorConnecting)).toHaveLength(1); expect(wrapper.find(EuiPage)).toHaveLength(0); - }); - - describe('access checks', () => { - it('does not render the App Search card if the user does not have access to AS', () => { - const wrapper = shallow( - - ); - - expect(wrapper.find(ProductCard)).toHaveLength(1); - expect(wrapper.find(ProductCard).prop('product').ID).toEqual('workplaceSearch'); - }); - - it('does not render the Workplace Search card if the user does not have access to WS', () => { - const wrapper = shallow( - - ); - - expect(wrapper.find(ProductCard)).toHaveLength(1); - expect(wrapper.find(ProductCard).prop('product').ID).toEqual('appSearch'); - }); - - it('does not render any cards if the user does not have access', () => { - const wrapper = shallow(); - - expect(wrapper.find(ProductCard)).toHaveLength(0); - }); + expect(wrapper.find(ProductSelector)).toHaveLength(0); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx index 3a3ba02e07058..e2c05434dd0bb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx @@ -4,81 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useContext } from 'react'; +import { Route, Switch } from 'react-router-dom'; import { useValues } from 'kea'; -import { - EuiPage, - EuiPageBody, - EuiPageHeader, - EuiPageHeaderSection, - EuiPageContentBody, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { KibanaContext, IKibanaContext } from '../index'; import { IInitialAppData } from '../../../common/types'; -import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../common/constants'; import { HttpLogic } from '../shared/http'; -import { SetEnterpriseSearchChrome as SetPageChrome } from '../shared/kibana_chrome'; -import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../shared/telemetry'; + +import { ROOT_PATH, SETUP_GUIDE_PATH } from './routes'; import { ErrorConnecting } from './components/error_connecting'; -import { ProductCard } from './components/product_card'; +import { ProductSelector } from './components/product_selector'; +import { SetupGuide } from './components/setup_guide'; -import AppSearchImage from './assets/app_search.png'; -import WorkplaceSearchImage from './assets/workplace_search.png'; import './index.scss'; export const EnterpriseSearch: React.FC = ({ access = {} }) => { const { errorConnecting } = useValues(HttpLogic); - const { hasAppSearchAccess, hasWorkplaceSearchAccess } = access; - - return errorConnecting ? ( - - ) : ( - - - - - - - - -

    - {i18n.translate('xpack.enterpriseSearch.overview.heading', { - defaultMessage: 'Welcome to Elastic Enterprise Search', - })} -

    -
    - -

    - {i18n.translate('xpack.enterpriseSearch.overview.subheading', { - defaultMessage: 'Select a product to get started', - })} -

    -
    -
    -
    - - - {hasAppSearchAccess && ( - - - - )} - {hasWorkplaceSearchAccess && ( - - - - )} - - - -
    -
    + const { config } = useContext(KibanaContext) as IKibanaContext; + + const showErrorConnecting = config.host && errorConnecting; + + return ( + + + + + + {showErrorConnecting ? : } + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/routes.ts new file mode 100644 index 0000000000000..1f9c06e9683ab --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/routes.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; + * you may not use this file except in compliance with the Elastic License. + */ + +export const ROOT_PATH = '/'; +export const SETUP_GUIDE_PATH = '/setup_guide'; From d4232c5b028c6b06232c8f43a85c25f312d7911a Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 23 Sep 2020 22:40:03 -0700 Subject: [PATCH 78/92] skip security solution tests that are preventing es snapshot promotion (#78366) Co-authored-by: spalger --- .../apis/epm/install_remove_assets.ts | 3 ++- .../apis/epm/update_assets.ts | 3 ++- .../apps/endpoint/endpoint_list.ts | 9 +++++---- .../apis/artifacts/index.ts | 1 + .../security_solution_endpoint_api_int/apis/metadata.ts | 1 + 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts index 198c129b7482f..492af399d5e30 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts @@ -29,7 +29,8 @@ export default function (providerContext: FtrProviderContext) { .send({ force: true }); }; - describe('installs and uninstalls all assets', async () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/72102 + describe.skip('installs and uninstalls all assets', async () => { describe('installs all assets when installing a package for the first time', async () => { skipIfNoDockerRegistry(providerContext); before(async () => { diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts index 9af27f5f98558..8203b4d183871 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts @@ -32,7 +32,8 @@ export default function (providerContext: FtrProviderContext) { .send({ force: true }); }; - describe('updates all assets when updating a package to a different version', async () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/72102 + describe.skip('updates all assets when updating a package to a different version', async () => { skipIfNoDockerRegistry(providerContext); before(async () => { await installPackage(pkgKey); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index d46171bbaa49f..c9d2b7a21d0da 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -65,7 +65,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ], ]; - describe('endpoint list', function () { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/72102 + describe.skip('endpoint list', function () { this.tags('ciGroup7'); const sleep = (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -86,7 +87,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.exists('emptyPolicyTable'); }); - it.skip('finds data after load and polling', async () => { + it('finds data after load and polling', async () => { await esArchiver.load('endpoint/metadata/destination_index', { useCreate: true }); await pageObjects.endpoint.waitForTableToHaveData('endpointListTable', 1100); const tableData = await pageObjects.endpointPageUtils.tableData('endpointListTable'); @@ -94,7 +95,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - describe.skip('when there is data,', () => { + describe('when there is data,', () => { before(async () => { await esArchiver.load('endpoint/metadata/destination_index', { useCreate: true }); await pageObjects.endpoint.navigateToEndpointList(); @@ -212,7 +213,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - describe.skip('displays the correct table data for the kql queries', () => { + describe('displays the correct table data for the kql queries', () => { before(async () => { await esArchiver.load('endpoint/metadata/destination_index', { useCreate: true }); await pageObjects.endpoint.navigateToEndpointList(); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts index 6c225dea5430f..5a4053ee6f0a9 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts @@ -18,6 +18,7 @@ export default function (providerContext: FtrProviderContext) { const supertestWithoutAuth = getSupertestWithoutAuth(providerContext); let agentAccessAPIKey: string; + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/72102 describe.skip('artifact download', () => { before(async () => { await esArchiver.load('endpoint/artifacts/api_feature', { useCreate: true }); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index d1e98876596e5..2ab12e1ff3aae 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -23,6 +23,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/72102 describe.skip('test metadata api', () => { describe(`POST ${METADATA_REQUEST_ROUTE} when index is empty`, () => { it('metadata api should return empty result when index is empty', async () => { From 9b1883d51e85ccda89be42e74368d3b9de69866c Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 23 Sep 2020 23:30:43 -0700 Subject: [PATCH 79/92] skip flaky suite (#78375) --- .../test/security_solution_endpoint/apps/endpoint/resolver.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts index 620eab37f9b46..3e9726bf40073 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts @@ -13,7 +13,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const queryBar = getService('queryBar'); - describe('Endpoint Event Resolver', function () { + // FLAKY: https://github.com/elastic/kibana/issues/78375 + describe.skip('Endpoint Event Resolver', function () { before(async () => { await esArchiver.load('endpoint/resolver_tree', { useCreate: true }); await pageObjects.hosts.navigateToSecurityHostsPage(); From c02e42ad01d35753e595c89056df56393bc19c2d Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 23 Sep 2020 23:35:45 -0700 Subject: [PATCH 80/92] skip flaky suite (#78373) --- test/functional/apps/discover/_doc_navigation.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/functional/apps/discover/_doc_navigation.js b/test/functional/apps/discover/_doc_navigation.js index 5ae799f8756c0..31aef96918ffa 100644 --- a/test/functional/apps/discover/_doc_navigation.js +++ b/test/functional/apps/discover/_doc_navigation.js @@ -28,8 +28,8 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const retry = getService('retry'); - // Flaky: https://github.com/elastic/kibana/issues/71216 - describe('doc link in discover', function contextSize() { + // FLAKY: https://github.com/elastic/kibana/issues/78373 + describe.skip('doc link in discover', function contextSize() { beforeEach(async function () { log.debug('load kibana index with default index pattern'); await esArchiver.loadIfNeeded('discover'); From 477f6a182ed4d4f97b4555c76a73781f788d22a2 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 24 Sep 2020 09:10:00 +0200 Subject: [PATCH 81/92] [CSM] Fix pie chart legend (#78253) --- .../Charts/VisitorBreakdownChart.tsx | 11 +++- .../app/RumDashboard/RumDashboard.tsx | 18 +++--- .../lib/rum_client/get_page_view_trends.ts | 4 +- .../lib/rum_client/get_visitor_breakdown.ts | 60 ++++++++++++------- 4 files changed, 59 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx index 34fcf62178711..dea6525d4be5f 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx @@ -10,6 +10,7 @@ import { DARK_THEME, Datum, LIGHT_THEME, + PartialTheme, Partition, PartitionLayout, Settings, @@ -34,6 +35,12 @@ interface Props { loading: boolean; } +const theme: PartialTheme = { + legend: { + verticalWidth: 100, + }, +}; + export function VisitorBreakdownChart({ loading, options }: Props) { const [darkMode] = useUiSetting$('theme:darkMode'); @@ -42,13 +49,13 @@ export function VisitorBreakdownChart({ loading, options }: Props) { : EUI_CHARTS_THEME_LIGHT; return ( - + - - - + + + - - + + + + - - - + + + ); diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts index ef4f8b16e0e7b..352a3ecdc3f12 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -46,7 +46,7 @@ export async function getPageViewTrends({ terms: { field: breakdownItem.fieldName, size: 9, - missing: 'Other', + missing: 'Others', }, }, } @@ -103,7 +103,7 @@ export async function getPageViewTrends({ }); // Top 9 plus others, get a diff from parent bucket total if (bCount > top9Count) { - res.Other = bCount - top9Count; + res.Others = bCount - top9Count; } } diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts index 1b4388afd7c5d..7345d6acc0f82 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts @@ -12,7 +12,6 @@ import { SetupUIFilters, } from '../helpers/setup_request'; import { - USER_AGENT_DEVICE, USER_AGENT_NAME, USER_AGENT_OS, } from '../../../common/elasticsearch_fieldnames'; @@ -32,6 +31,7 @@ export async function getVisitorBreakdown({ const params = mergeProjection(projection, { body: { size: 0, + track_total_hits: true, query: { bool: projection.body.query.bool, }, @@ -39,19 +39,13 @@ export async function getVisitorBreakdown({ browsers: { terms: { field: USER_AGENT_NAME, - size: 10, + size: 9, }, }, os: { terms: { field: USER_AGENT_OS, - size: 10, - }, - }, - devices: { - terms: { - field: USER_AGENT_DEVICE, - size: 10, + size: 9, }, }, }, @@ -61,20 +55,42 @@ export async function getVisitorBreakdown({ const { apmEventClient } = setup; const response = await apmEventClient.search(params); - const { browsers, os, devices } = response.aggregations!; + const { browsers, os } = response.aggregations!; + + const totalItems = response.hits.total.value; + + const browserTotal = browsers.buckets.reduce( + (prevVal, item) => prevVal + item.doc_count, + 0 + ); + + const osTotal = os.buckets.reduce( + (prevVal, item) => prevVal + item.doc_count, + 0 + ); + + const browserItems = browsers.buckets.map((bucket) => ({ + count: bucket.doc_count, + name: bucket.key as string, + })); + + browserItems.push({ + count: totalItems - browserTotal, + name: 'Others', + }); + + const osItems = os.buckets.map((bucket) => ({ + count: bucket.doc_count, + name: bucket.key as string, + })); + + osItems.push({ + count: totalItems - osTotal, + name: 'Others', + }); return { - browsers: browsers.buckets.map((bucket) => ({ - count: bucket.doc_count, - name: bucket.key as string, - })), - os: os.buckets.map((bucket) => ({ - count: bucket.doc_count, - name: bucket.key as string, - })), - devices: devices.buckets.map((bucket) => ({ - count: bucket.doc_count, - name: bucket.key as string, - })), + os: osItems, + browsers: browserItems, }; } From 62ddaa9e205ff49d4deb9912025e510513c49ee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Thu, 24 Sep 2020 09:36:22 +0200 Subject: [PATCH 82/92] [Security Solution] Cleanup IP Details graphql (#78318) --- .../security_solution/network/users/index.ts | 6 +- .../public/graphql/introspection.json | 1008 ++++------------- .../security_solution/public/graphql/types.ts | 398 +------ .../components/users_table/columns.tsx | 12 +- .../network/components/users_table/mock.ts | 5 +- .../containers/details/index.gql_query.ts | 91 -- .../containers/users/index.gql_query.ts | 59 - .../security_solution/server/graphql/index.ts | 2 - .../server/graphql/ip_details/index.ts | 8 - .../server/graphql/ip_details/resolvers.ts | 50 - .../server/graphql/ip_details/schema.gql.ts | 97 -- .../security_solution/server/graphql/types.ts | 409 +------ .../security_solution/server/init_server.ts | 2 - .../server/lib/compose/kibana.ts | 2 - .../ip_details/elasticsearch_adapter.test.ts | 53 - .../lib/ip_details/elasticsearch_adapter.ts | 160 --- .../server/lib/ip_details/index.ts | 37 - .../server/lib/ip_details/mock.ts | 430 ------- .../lib/ip_details/query_overview.dsl.ts | 126 --- .../server/lib/ip_details/query_users.dsl.ts | 104 -- .../server/lib/ip_details/types.ts | 135 --- .../security_solution/server/lib/types.ts | 2 - .../apis/security_solution/index.js | 2 +- .../apis/security_solution/network_details.ts | 2 + .../apis/security_solution/users.ts | 3 + 25 files changed, 258 insertions(+), 2945 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/network/containers/details/index.gql_query.ts delete mode 100644 x-pack/plugins/security_solution/public/network/containers/users/index.gql_query.ts delete mode 100644 x-pack/plugins/security_solution/server/graphql/ip_details/index.ts delete mode 100644 x-pack/plugins/security_solution/server/graphql/ip_details/resolvers.ts delete mode 100644 x-pack/plugins/security_solution/server/graphql/ip_details/schema.gql.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.test.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/ip_details/index.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/ip_details/mock.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/ip_details/query_overview.dsl.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/ip_details/types.ts diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/users/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/users/index.ts index 196317e7587bf..8c4e19a804148 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/users/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/users/index.ts @@ -40,9 +40,9 @@ export interface NetworkUsersNode { export interface NetworkUsersItem { name?: Maybe; - id?: Maybe; - groupId?: Maybe; - groupName?: Maybe; + id?: Maybe; + groupId?: Maybe; + groupName?: Maybe; count?: Maybe; } diff --git a/x-pack/plugins/security_solution/public/graphql/introspection.json b/x-pack/plugins/security_solution/public/graphql/introspection.json index 2f312c461ff8c..ece0712414349 100644 --- a/x-pack/plugins/security_solution/public/graphql/introspection.json +++ b/x-pack/plugins/security_solution/public/graphql/introspection.json @@ -1245,174 +1245,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "IpOverview", - "description": "", - "args": [ - { - "name": "id", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "filterQuery", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "ip", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "defaultIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "defaultValue": null - }, - { - "name": "docValueFields", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "docValueFieldsInput", - "ofType": null - } - } - } - }, - "defaultValue": null - } - ], - "type": { "kind": "OBJECT", "name": "IpOverviewData", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "Users", - "description": "", - "args": [ - { - "name": "filterQuery", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "id", - "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - }, - { - "name": "ip", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "pagination", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "PaginationInputPaginated", - "ofType": null - } - }, - "defaultValue": null - }, - { - "name": "sort", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "UsersSortField", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "flowTarget", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "ENUM", "name": "FlowTarget", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "timerange", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "defaultIndex", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - } - } - }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "UsersData", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "KpiNetwork", "description": "", @@ -6170,594 +6002,32 @@ { "name": "ip", "description": "", - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "LastEventTimeData", - "description": "", - "fields": [ - { - "name": "lastSeen", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "HostsSortField", - "description": "", - "fields": null, - "inputFields": [ - { - "name": "field", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "ENUM", "name": "HostsFields", "ofType": null } - }, - "defaultValue": null - }, - { - "name": "direction", - "description": "", - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "ENUM", "name": "Direction", "ofType": null } - }, - "defaultValue": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "HostsFields", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "hostName", - "description": "", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lastSeen", - "description": "", - "isDeprecated": false, - "deprecationReason": null - } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "HostsData", - "description": "", - "fields": [ - { - "name": "edges", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "HostsEdges", "ofType": null } - } - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "totalCount", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "PageInfoPaginated", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "HostsEdges", - "description": "", - "fields": [ - { - "name": "node", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "HostItem", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "cursor", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "CursorType", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "HostItem", - "description": "", - "fields": [ - { - "name": "_id", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "cloud", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "CloudFields", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "endpoint", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "EndpointFields", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "host", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "HostEcsFields", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lastSeen", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CloudFields", - "description": "", - "fields": [ - { - "name": "instance", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "CloudInstance", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "machine", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "CloudMachine", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "provider", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "region", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CloudInstance", - "description": "", - "fields": [ - { - "name": "id", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "CloudMachine", - "description": "", - "fields": [ - { - "name": "type", - "description": "", - "args": [], - "type": { - "kind": "LIST", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "EndpointFields", - "description": "", - "fields": [ - { - "name": "endpointPolicy", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "sensorVersion", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "policyStatus", - "description": "", - "args": [], - "type": { "kind": "ENUM", "name": "HostPolicyResponseActionStatus", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "HostPolicyResponseActionStatus", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { - "name": "success", - "description": "", - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "failure", - "description": "", - "isDeprecated": false, - "deprecationReason": null - }, - { "name": "warning", "description": "", "isDeprecated": false, "deprecationReason": null } - ], - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "FirstLastSeenHost", - "description": "", - "fields": [ - { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "firstSeen", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lastSeen", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "IpOverviewData", - "description": "", - "fields": [ - { - "name": "client", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Overview", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "destination", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Overview", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "host", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "HostEcsFields", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "server", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Overview", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "source", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Overview", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "inspect", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "Overview", - "description": "", - "fields": [ - { - "name": "firstSeen", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "lastSeen", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "autonomousSystem", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "AutonomousSystem", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "geo", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "OBJECT", "name": "GeoEcsFields", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "AutonomousSystem", - "description": "", - "fields": [ - { - "name": "number", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "organization", - "description": "", - "args": [], - "type": { "kind": "OBJECT", "name": "AutonomousSystemOrganization", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null } ], - "inputFields": null, - "interfaces": [], + "interfaces": null, "enumValues": null, "possibleTypes": null }, { "kind": "OBJECT", - "name": "AutonomousSystemOrganization", + "name": "LastEventTimeData", "description": "", "fields": [ { - "name": "name", + "name": "lastSeen", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inspect", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, "isDeprecated": false, "deprecationReason": null } @@ -6769,7 +6039,7 @@ }, { "kind": "INPUT_OBJECT", - "name": "UsersSortField", + "name": "HostsSortField", "description": "", "fields": null, "inputFields": [ @@ -6779,7 +6049,7 @@ "type": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "ENUM", "name": "UsersFields", "ofType": null } + "ofType": { "kind": "ENUM", "name": "HostsFields", "ofType": null } }, "defaultValue": null }, @@ -6800,40 +6070,30 @@ }, { "kind": "ENUM", - "name": "UsersFields", - "description": "", - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": [ - { "name": "name", "description": "", "isDeprecated": false, "deprecationReason": null }, - { "name": "count", "description": "", "isDeprecated": false, "deprecationReason": null } - ], - "possibleTypes": null - }, - { - "kind": "ENUM", - "name": "FlowTarget", + "name": "HostsFields", "description": "", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [ - { "name": "client", "description": "", "isDeprecated": false, "deprecationReason": null }, { - "name": "destination", + "name": "hostName", "description": "", "isDeprecated": false, "deprecationReason": null }, - { "name": "server", "description": "", "isDeprecated": false, "deprecationReason": null }, - { "name": "source", "description": "", "isDeprecated": false, "deprecationReason": null } + { + "name": "lastSeen", + "description": "", + "isDeprecated": false, + "deprecationReason": null + } ], "possibleTypes": null }, { "kind": "OBJECT", - "name": "UsersData", + "name": "HostsData", "description": "", "fields": [ { @@ -6849,7 +6109,7 @@ "ofType": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "OBJECT", "name": "UsersEdges", "ofType": null } + "ofType": { "kind": "OBJECT", "name": "HostsEdges", "ofType": null } } } }, @@ -6896,7 +6156,7 @@ }, { "kind": "OBJECT", - "name": "UsersEdges", + "name": "HostsEdges", "description": "", "fields": [ { @@ -6906,7 +6166,7 @@ "type": { "kind": "NON_NULL", "name": null, - "ofType": { "kind": "OBJECT", "name": "UsersNode", "ofType": null } + "ofType": { "kind": "OBJECT", "name": "HostItem", "ofType": null } }, "isDeprecated": false, "deprecationReason": null @@ -6931,7 +6191,7 @@ }, { "kind": "OBJECT", - "name": "UsersNode", + "name": "HostItem", "description": "", "fields": [ { @@ -6943,18 +6203,116 @@ "deprecationReason": null }, { - "name": "timestamp", + "name": "cloud", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "CloudFields", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "endpoint", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "EndpointFields", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "host", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "HostEcsFields", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inspect", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastSeen", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, "isDeprecated": false, "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CloudFields", + "description": "", + "fields": [ + { + "name": "instance", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "CloudInstance", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null }, { - "name": "user", + "name": "machine", + "description": "", + "args": [], + "type": { "kind": "OBJECT", "name": "CloudMachine", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "provider", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "region", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CloudInstance", + "description": "", + "fields": [ + { + "name": "id", "description": "", "args": [], - "type": { "kind": "OBJECT", "name": "UsersItem", "ofType": null }, + "type": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, "isDeprecated": false, "deprecationReason": null } @@ -6966,11 +6324,34 @@ }, { "kind": "OBJECT", - "name": "UsersItem", + "name": "CloudMachine", "description": "", "fields": [ { - "name": "name", + "name": "type", + "description": "", + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "EndpointFields", + "description": "", + "fields": [ + { + "name": "endpointPolicy", "description": "", "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, @@ -6978,34 +6359,77 @@ "deprecationReason": null }, { - "name": "id", + "name": "sensorVersion", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "groupId", + "name": "policyStatus", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "type": { "kind": "ENUM", "name": "HostPolicyResponseActionStatus", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "HostPolicyResponseActionStatus", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "success", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "failure", + "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "warning", "description": "", "isDeprecated": false, "deprecationReason": null } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "FirstLastSeenHost", + "description": "", + "fields": [ { - "name": "groupName", + "name": "inspect", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "type": { "kind": "OBJECT", "name": "Inspect", "ofType": null }, "isDeprecated": false, "deprecationReason": null }, { - "name": "count", + "name": "firstSeen", "description": "", "args": [], - "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "lastSeen", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Date", "ofType": null }, "isDeprecated": false, "deprecationReason": null } @@ -12242,6 +11666,26 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "ENUM", + "name": "FlowTarget", + "description": "", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { "name": "client", "description": "", "isDeprecated": false, "deprecationReason": null }, + { + "name": "destination", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "server", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "source", "description": "", "isDeprecated": false, "deprecationReason": null } + ], + "possibleTypes": null + }, { "kind": "ENUM", "name": "FlowDirection", diff --git a/x-pack/plugins/security_solution/public/graphql/types.ts b/x-pack/plugins/security_solution/public/graphql/types.ts index bcb580a1a2988..1083583cb133c 100644 --- a/x-pack/plugins/security_solution/public/graphql/types.ts +++ b/x-pack/plugins/security_solution/public/graphql/types.ts @@ -73,12 +73,6 @@ export interface HostsSortField { direction: Direction; } -export interface UsersSortField { - field: UsersFields; - - direction: Direction; -} - export interface NetworkTopTablesSortField { field: NetworkTopTablesFields; @@ -309,18 +303,6 @@ export enum HostPolicyResponseActionStatus { warning = 'warning', } -export enum UsersFields { - name = 'name', - count = 'count', -} - -export enum FlowTarget { - client = 'client', - destination = 'destination', - server = 'server', - source = 'source', -} - export enum HistogramType { authentications = 'authentications', anomalies = 'anomalies', @@ -410,6 +392,13 @@ export enum NetworkHttpFields { statuses = 'statuses', } +export enum FlowTarget { + client = 'client', + destination = 'destination', + server = 'server', + source = 'source', +} + export enum FlowDirection { uniDirectional = 'uniDirectional', biDirectional = 'biDirectional', @@ -535,10 +524,6 @@ export interface Source { HostFirstLastSeen: FirstLastSeenHost; - IpOverview?: Maybe; - - Users: UsersData; - KpiNetwork?: Maybe; KpiHosts: KpiHostsData; @@ -1462,76 +1447,6 @@ export interface FirstLastSeenHost { lastSeen?: Maybe; } -export interface IpOverviewData { - client?: Maybe; - - destination?: Maybe; - - host: HostEcsFields; - - server?: Maybe; - - source?: Maybe; - - inspect?: Maybe; -} - -export interface Overview { - firstSeen?: Maybe; - - lastSeen?: Maybe; - - autonomousSystem: AutonomousSystem; - - geo: GeoEcsFields; -} - -export interface AutonomousSystem { - number?: Maybe; - - organization?: Maybe; -} - -export interface AutonomousSystemOrganization { - name?: Maybe; -} - -export interface UsersData { - edges: UsersEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe; -} - -export interface UsersEdges { - node: UsersNode; - - cursor: CursorType; -} - -export interface UsersNode { - _id?: Maybe; - - timestamp?: Maybe; - - user?: Maybe; -} - -export interface UsersItem { - name?: Maybe; - - id?: Maybe; - - groupId?: Maybe; - - groupName?: Maybe; - - count?: Maybe; -} - export interface KpiNetworkData { networkEvents?: Maybe; @@ -2282,34 +2197,6 @@ export interface HostFirstLastSeenSourceArgs { docValueFields: DocValueFieldsInput[]; } -export interface IpOverviewSourceArgs { - id?: Maybe; - - filterQuery?: Maybe; - - ip: string; - - defaultIndex: string[]; - - docValueFields: DocValueFieldsInput[]; -} -export interface UsersSourceArgs { - filterQuery?: Maybe; - - id?: Maybe; - - ip: string; - - pagination: PaginationInputPaginated; - - sort: UsersSortField; - - flowTarget: FlowTarget; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} export interface KpiNetworkSourceArgs { id?: Maybe; @@ -3071,185 +2958,6 @@ export namespace GetKpiHostsQuery { }; } -export namespace GetIpOverviewQuery { - export type Variables = { - sourceId: string; - filterQuery?: Maybe; - ip: string; - defaultIndex: string[]; - inspect: boolean; - docValueFields: DocValueFieldsInput[]; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - IpOverview: Maybe; - }; - - export type IpOverview = { - __typename?: 'IpOverviewData'; - - source: Maybe<_Source>; - - destination: Maybe; - - host: Host; - - inspect: Maybe; - }; - - export type _Source = { - __typename?: 'Overview'; - - firstSeen: Maybe; - - lastSeen: Maybe; - - autonomousSystem: AutonomousSystem; - - geo: Geo; - }; - - export type AutonomousSystem = { - __typename?: 'AutonomousSystem'; - - number: Maybe; - - organization: Maybe; - }; - - export type Organization = { - __typename?: 'AutonomousSystemOrganization'; - - name: Maybe; - }; - - export type Geo = { - __typename?: 'GeoEcsFields'; - - continent_name: Maybe; - - city_name: Maybe; - - country_iso_code: Maybe; - - country_name: Maybe; - - location: Maybe; - - region_iso_code: Maybe; - - region_name: Maybe; - }; - - export type Location = { - __typename?: 'Location'; - - lat: Maybe; - - lon: Maybe; - }; - - export type Destination = { - __typename?: 'Overview'; - - firstSeen: Maybe; - - lastSeen: Maybe; - - autonomousSystem: _AutonomousSystem; - - geo: _Geo; - }; - - export type _AutonomousSystem = { - __typename?: 'AutonomousSystem'; - - number: Maybe; - - organization: Maybe<_Organization>; - }; - - export type _Organization = { - __typename?: 'AutonomousSystemOrganization'; - - name: Maybe; - }; - - export type _Geo = { - __typename?: 'GeoEcsFields'; - - continent_name: Maybe; - - city_name: Maybe; - - country_iso_code: Maybe; - - country_name: Maybe; - - location: Maybe<_Location>; - - region_iso_code: Maybe; - - region_name: Maybe; - }; - - export type _Location = { - __typename?: 'Location'; - - lat: Maybe; - - lon: Maybe; - }; - - export type Host = { - __typename?: 'HostEcsFields'; - - architecture: Maybe; - - id: Maybe; - - ip: Maybe; - - mac: Maybe; - - name: Maybe; - - os: Maybe; - - type: Maybe; - }; - - export type Os = { - __typename?: 'OsEcsFields'; - - family: Maybe; - - name: Maybe; - - platform: Maybe; - - version: Maybe; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - export namespace GetKpiNetworkQuery { export type Variables = { sourceId: string; @@ -3785,98 +3493,6 @@ export namespace GetNetworkTopNFlowQuery { }; } -export namespace GetUsersQuery { - export type Variables = { - sourceId: string; - filterQuery?: Maybe; - flowTarget: FlowTarget; - ip: string; - pagination: PaginationInputPaginated; - sort: UsersSortField; - timerange: TimerangeInput; - defaultIndex: string[]; - inspect: boolean; - }; - - export type Query = { - __typename?: 'Query'; - - source: Source; - }; - - export type Source = { - __typename?: 'Source'; - - id: string; - - Users: Users; - }; - - export type Users = { - __typename?: 'UsersData'; - - totalCount: number; - - edges: Edges[]; - - pageInfo: PageInfo; - - inspect: Maybe; - }; - - export type Edges = { - __typename?: 'UsersEdges'; - - node: Node; - - cursor: Cursor; - }; - - export type Node = { - __typename?: 'UsersNode'; - - user: Maybe; - }; - - export type User = { - __typename?: 'UsersItem'; - - name: Maybe; - - id: Maybe; - - groupId: Maybe; - - groupName: Maybe; - - count: Maybe; - }; - - export type Cursor = { - __typename?: 'CursorType'; - - value: Maybe; - }; - - export type PageInfo = { - __typename?: 'PageInfoPaginated'; - - activePage: number; - - fakeTotalCount: number; - - showMorePagesIndicator: boolean; - }; - - export type Inspect = { - __typename?: 'Inspect'; - - dsl: string[]; - - response: string[]; - }; -} - export namespace GetAllTimeline { export type Variables = { pageInfo: PageInfoTimeline; diff --git a/x-pack/plugins/security_solution/public/network/components/users_table/columns.tsx b/x-pack/plugins/security_solution/public/network/components/users_table/columns.tsx index b7f7887342335..afef7fe794939 100644 --- a/x-pack/plugins/security_solution/public/network/components/users_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/network/components/users_table/columns.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FlowTarget, UsersItem } from '../../../graphql/types'; +import { FlowTarget, NetworkUsersItem } from '../../../../common/search_strategy'; import { defaultToEmptyTag } from '../../../common/components/empty_value'; import { Columns } from '../../../common/components/paginated_table'; @@ -15,11 +15,11 @@ import { } from '../../../common/components/tables/helpers'; export type UsersColumns = [ - Columns, - Columns, - Columns, - Columns, - Columns + Columns, + Columns, + Columns, + Columns, + Columns ]; export const getUsersColumns = (flowTarget: FlowTarget, tableId: string): UsersColumns => [ diff --git a/x-pack/plugins/security_solution/public/network/components/users_table/mock.ts b/x-pack/plugins/security_solution/public/network/components/users_table/mock.ts index 50bef1867aa3b..9180ee328f988 100644 --- a/x-pack/plugins/security_solution/public/network/components/users_table/mock.ts +++ b/x-pack/plugins/security_solution/public/network/components/users_table/mock.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UsersData } from '../../../graphql/types'; +import { NetworkUsersStrategyResponse } from '../../../../common/search_strategy'; -export const mockUsersData: UsersData = { +export const mockUsersData: NetworkUsersStrategyResponse = { edges: [ { node: { @@ -63,4 +63,5 @@ export const mockUsersData: UsersData = { fakeTotalCount: 3, showMorePagesIndicator: true, }, + rawResponse: {} as NetworkUsersStrategyResponse['rawResponse'], }; diff --git a/x-pack/plugins/security_solution/public/network/containers/details/index.gql_query.ts b/x-pack/plugins/security_solution/public/network/containers/details/index.gql_query.ts deleted file mode 100644 index 6ebb60ccb4ea6..0000000000000 --- a/x-pack/plugins/security_solution/public/network/containers/details/index.gql_query.ts +++ /dev/null @@ -1,91 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const ipOverviewQuery = gql` - query GetIpOverviewQuery( - $sourceId: ID! - $filterQuery: String - $ip: String! - $defaultIndex: [String!]! - $inspect: Boolean! - $docValueFields: [docValueFieldsInput!]! - ) { - source(id: $sourceId) { - id - IpOverview( - filterQuery: $filterQuery - ip: $ip - defaultIndex: $defaultIndex - docValueFields: $docValueFields - ) { - source { - firstSeen - lastSeen - autonomousSystem { - number - organization { - name - } - } - geo { - continent_name - city_name - country_iso_code - country_name - location { - lat - lon - } - region_iso_code - region_name - } - } - destination { - firstSeen - lastSeen - autonomousSystem { - number - organization { - name - } - } - geo { - continent_name - city_name - country_iso_code - country_name - location { - lat - lon - } - region_iso_code - region_name - } - } - host { - architecture - id - ip - mac - name - os { - family - name - platform - version - } - type - } - inspect @include(if: $inspect) { - dsl - response - } - } - } - } -`; diff --git a/x-pack/plugins/security_solution/public/network/containers/users/index.gql_query.ts b/x-pack/plugins/security_solution/public/network/containers/users/index.gql_query.ts deleted file mode 100644 index 3fc1cdfd160db..0000000000000 --- a/x-pack/plugins/security_solution/public/network/containers/users/index.gql_query.ts +++ /dev/null @@ -1,59 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const usersQuery = gql` - query GetUsersQuery( - $sourceId: ID! - $filterQuery: String - $flowTarget: FlowTarget! - $ip: String! - $pagination: PaginationInputPaginated! - $sort: UsersSortField! - $timerange: TimerangeInput! - $defaultIndex: [String!]! - $inspect: Boolean! - ) { - source(id: $sourceId) { - id - Users( - filterQuery: $filterQuery - flowTarget: $flowTarget - ip: $ip - pagination: $pagination - sort: $sort - timerange: $timerange - defaultIndex: $defaultIndex - ) { - totalCount - edges { - node { - user { - name - id - groupId - groupName - count - } - } - cursor { - value - } - } - pageInfo { - activePage - fakeTotalCount - showMorePagesIndicator - } - inspect @include(if: $inspect) { - dsl - response - } - } - } - } -`; diff --git a/x-pack/plugins/security_solution/server/graphql/index.ts b/x-pack/plugins/security_solution/server/graphql/index.ts index 2de6ef32b5703..d23494e0eeaa6 100644 --- a/x-pack/plugins/security_solution/server/graphql/index.ts +++ b/x-pack/plugins/security_solution/server/graphql/index.ts @@ -11,7 +11,6 @@ import { authenticationsSchema } from './authentications'; import { ecsSchema } from './ecs'; import { eventsSchema } from './events'; import { hostsSchema } from './hosts'; -import { ipDetailsSchemas } from './ip_details'; import { kpiHostsSchema } from './kpi_hosts'; import { kpiNetworkSchema } from './kpi_network'; import { networkSchema } from './network'; @@ -37,7 +36,6 @@ export const schemas = [ toDateSchema, toBooleanSchema, hostsSchema, - ...ipDetailsSchemas, kpiNetworkSchema, kpiHostsSchema, matrixHistogramSchema, diff --git a/x-pack/plugins/security_solution/server/graphql/ip_details/index.ts b/x-pack/plugins/security_solution/server/graphql/ip_details/index.ts deleted file mode 100644 index 186397ea347cb..0000000000000 --- a/x-pack/plugins/security_solution/server/graphql/ip_details/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { createIpDetailsResolvers } from './resolvers'; -export { ipDetailsSchemas } from './schema.gql'; diff --git a/x-pack/plugins/security_solution/server/graphql/ip_details/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/ip_details/resolvers.ts deleted file mode 100644 index d0e84026de473..0000000000000 --- a/x-pack/plugins/security_solution/server/graphql/ip_details/resolvers.ts +++ /dev/null @@ -1,50 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SourceResolvers } from '../../graphql/types'; -import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; -import { IpDetails, UsersRequestOptions } from '../../lib/ip_details'; -import { createOptions, createOptionsPaginated } from '../../utils/build_query/create_options'; -import { QuerySourceResolver } from '../sources/resolvers'; - -export type QueryIpOverviewResolver = ChildResolverOf< - AppResolverOf, - QuerySourceResolver ->; - -export type QueryUsersResolver = ChildResolverOf< - AppResolverOf, - QuerySourceResolver ->; - -export interface IDetailsResolversDeps { - ipDetails: IpDetails; -} - -export const createIpDetailsResolvers = ( - libs: IDetailsResolversDeps -): { - Source: { - IpOverview: QueryIpOverviewResolver; - Users: QueryUsersResolver; - }; -} => ({ - Source: { - async IpOverview(source, args, { req }, info) { - const options = { ...createOptions(source, args, info), ip: args.ip }; - return libs.ipDetails.getIpOverview(req, options); - }, - async Users(source, args, { req }, info) { - const options: UsersRequestOptions = { - ...createOptionsPaginated(source, args, info), - ip: args.ip, - sort: args.sort, - flowTarget: args.flowTarget, - }; - return libs.ipDetails.getUsers(req, options); - }, - }, -}); diff --git a/x-pack/plugins/security_solution/server/graphql/ip_details/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/ip_details/schema.gql.ts deleted file mode 100644 index 2531f8d169327..0000000000000 --- a/x-pack/plugins/security_solution/server/graphql/ip_details/schema.gql.ts +++ /dev/null @@ -1,97 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -const ipOverviewSchema = gql` - type AutonomousSystemOrganization { - name: String - } - - type AutonomousSystem { - number: Float - organization: AutonomousSystemOrganization - } - - type Overview { - firstSeen: Date - lastSeen: Date - autonomousSystem: AutonomousSystem! - geo: GeoEcsFields! - } - - type IpOverviewData { - client: Overview - destination: Overview - host: HostEcsFields! - server: Overview - source: Overview - inspect: Inspect - } - - extend type Source { - IpOverview( - id: String - filterQuery: String - ip: String! - defaultIndex: [String!]! - docValueFields: [docValueFieldsInput!]! - ): IpOverviewData - } -`; - -const usersSchema = gql` - enum UsersFields { - name - count - } - - input UsersSortField { - field: UsersFields! - direction: Direction! - } - - type UsersItem { - name: String - id: ToStringArray - groupId: ToStringArray - groupName: ToStringArray - count: Float - } - - type UsersNode { - _id: String - timestamp: Date - user: UsersItem - } - - type UsersEdges { - node: UsersNode! - cursor: CursorType! - } - - type UsersData { - edges: [UsersEdges!]! - totalCount: Float! - pageInfo: PageInfoPaginated! - inspect: Inspect - } - - extend type Source { - Users( - filterQuery: String - id: String - ip: String! - pagination: PaginationInputPaginated! - sort: UsersSortField! - flowTarget: FlowTarget! - timerange: TimerangeInput! - defaultIndex: [String!]! - ): UsersData! - } -`; - -export const ipDetailsSchemas = [ipOverviewSchema, usersSchema]; diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index d10dfb16a9b8a..5f370ab1b8c9f 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -75,12 +75,6 @@ export interface HostsSortField { direction: Direction; } -export interface UsersSortField { - field: UsersFields; - - direction: Direction; -} - export interface NetworkTopTablesSortField { field: NetworkTopTablesFields; @@ -311,18 +305,6 @@ export enum HostPolicyResponseActionStatus { warning = 'warning', } -export enum UsersFields { - name = 'name', - count = 'count', -} - -export enum FlowTarget { - client = 'client', - destination = 'destination', - server = 'server', - source = 'source', -} - export enum HistogramType { authentications = 'authentications', anomalies = 'anomalies', @@ -412,6 +394,13 @@ export enum NetworkHttpFields { statuses = 'statuses', } +export enum FlowTarget { + client = 'client', + destination = 'destination', + server = 'server', + source = 'source', +} + export enum FlowDirection { uniDirectional = 'uniDirectional', biDirectional = 'biDirectional', @@ -537,10 +526,6 @@ export interface Source { HostFirstLastSeen: FirstLastSeenHost; - IpOverview?: Maybe; - - Users: UsersData; - KpiNetwork?: Maybe; KpiHosts: KpiHostsData; @@ -1464,76 +1449,6 @@ export interface FirstLastSeenHost { lastSeen?: Maybe; } -export interface IpOverviewData { - client?: Maybe; - - destination?: Maybe; - - host: HostEcsFields; - - server?: Maybe; - - source?: Maybe; - - inspect?: Maybe; -} - -export interface Overview { - firstSeen?: Maybe; - - lastSeen?: Maybe; - - autonomousSystem: AutonomousSystem; - - geo: GeoEcsFields; -} - -export interface AutonomousSystem { - number?: Maybe; - - organization?: Maybe; -} - -export interface AutonomousSystemOrganization { - name?: Maybe; -} - -export interface UsersData { - edges: UsersEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe; -} - -export interface UsersEdges { - node: UsersNode; - - cursor: CursorType; -} - -export interface UsersNode { - _id?: Maybe; - - timestamp?: Maybe; - - user?: Maybe; -} - -export interface UsersItem { - name?: Maybe; - - id?: Maybe; - - groupId?: Maybe; - - groupName?: Maybe; - - count?: Maybe; -} - export interface KpiNetworkData { networkEvents?: Maybe; @@ -2284,34 +2199,6 @@ export interface HostFirstLastSeenSourceArgs { docValueFields: DocValueFieldsInput[]; } -export interface IpOverviewSourceArgs { - id?: Maybe; - - filterQuery?: Maybe; - - ip: string; - - defaultIndex: string[]; - - docValueFields: DocValueFieldsInput[]; -} -export interface UsersSourceArgs { - filterQuery?: Maybe; - - id?: Maybe; - - ip: string; - - pagination: PaginationInputPaginated; - - sort: UsersSortField; - - flowTarget: FlowTarget; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} export interface KpiNetworkSourceArgs { id?: Maybe; @@ -2838,10 +2725,6 @@ export namespace SourceResolvers { HostFirstLastSeen?: HostFirstLastSeenResolver; - IpOverview?: IpOverviewResolver, TypeParent, TContext>; - - Users?: UsersResolver; - KpiNetwork?: KpiNetworkResolver, TypeParent, TContext>; KpiHosts?: KpiHostsResolver; @@ -3004,47 +2887,6 @@ export namespace SourceResolvers { docValueFields: DocValueFieldsInput[]; } - export type IpOverviewResolver< - R = Maybe, - Parent = Source, - TContext = SiemContext - > = Resolver; - export interface IpOverviewArgs { - id?: Maybe; - - filterQuery?: Maybe; - - ip: string; - - defaultIndex: string[]; - - docValueFields: DocValueFieldsInput[]; - } - - export type UsersResolver = Resolver< - R, - Parent, - TContext, - UsersArgs - >; - export interface UsersArgs { - filterQuery?: Maybe; - - id?: Maybe; - - ip: string; - - pagination: PaginationInputPaginated; - - sort: UsersSortField; - - flowTarget: FlowTarget; - - timerange: TimerangeInput; - - defaultIndex: string[]; - } - export type KpiNetworkResolver< R = Maybe, Parent = Source, @@ -6223,235 +6065,6 @@ export namespace FirstLastSeenHostResolvers { > = Resolver; } -export namespace IpOverviewDataResolvers { - export interface Resolvers { - client?: ClientResolver, TypeParent, TContext>; - - destination?: DestinationResolver, TypeParent, TContext>; - - host?: HostResolver; - - server?: ServerResolver, TypeParent, TContext>; - - source?: SourceResolver, TypeParent, TContext>; - - inspect?: InspectResolver, TypeParent, TContext>; - } - - export type ClientResolver< - R = Maybe, - Parent = IpOverviewData, - TContext = SiemContext - > = Resolver; - export type DestinationResolver< - R = Maybe, - Parent = IpOverviewData, - TContext = SiemContext - > = Resolver; - export type HostResolver< - R = HostEcsFields, - Parent = IpOverviewData, - TContext = SiemContext - > = Resolver; - export type ServerResolver< - R = Maybe, - Parent = IpOverviewData, - TContext = SiemContext - > = Resolver; - export type SourceResolver< - R = Maybe, - Parent = IpOverviewData, - TContext = SiemContext - > = Resolver; - export type InspectResolver< - R = Maybe, - Parent = IpOverviewData, - TContext = SiemContext - > = Resolver; -} - -export namespace OverviewResolvers { - export interface Resolvers { - firstSeen?: FirstSeenResolver, TypeParent, TContext>; - - lastSeen?: LastSeenResolver, TypeParent, TContext>; - - autonomousSystem?: AutonomousSystemResolver; - - geo?: GeoResolver; - } - - export type FirstSeenResolver< - R = Maybe, - Parent = Overview, - TContext = SiemContext - > = Resolver; - export type LastSeenResolver< - R = Maybe, - Parent = Overview, - TContext = SiemContext - > = Resolver; - export type AutonomousSystemResolver< - R = AutonomousSystem, - Parent = Overview, - TContext = SiemContext - > = Resolver; - export type GeoResolver = Resolver< - R, - Parent, - TContext - >; -} - -export namespace AutonomousSystemResolvers { - export interface Resolvers { - number?: NumberResolver, TypeParent, TContext>; - - organization?: OrganizationResolver, TypeParent, TContext>; - } - - export type NumberResolver< - R = Maybe, - Parent = AutonomousSystem, - TContext = SiemContext - > = Resolver; - export type OrganizationResolver< - R = Maybe, - Parent = AutonomousSystem, - TContext = SiemContext - > = Resolver; -} - -export namespace AutonomousSystemOrganizationResolvers { - export interface Resolvers { - name?: NameResolver, TypeParent, TContext>; - } - - export type NameResolver< - R = Maybe, - Parent = AutonomousSystemOrganization, - TContext = SiemContext - > = Resolver; -} - -export namespace UsersDataResolvers { - export interface Resolvers { - edges?: EdgesResolver; - - totalCount?: TotalCountResolver; - - pageInfo?: PageInfoResolver; - - inspect?: InspectResolver, TypeParent, TContext>; - } - - export type EdgesResolver< - R = UsersEdges[], - Parent = UsersData, - TContext = SiemContext - > = Resolver; - export type TotalCountResolver = Resolver< - R, - Parent, - TContext - >; - export type PageInfoResolver< - R = PageInfoPaginated, - Parent = UsersData, - TContext = SiemContext - > = Resolver; - export type InspectResolver< - R = Maybe, - Parent = UsersData, - TContext = SiemContext - > = Resolver; -} - -export namespace UsersEdgesResolvers { - export interface Resolvers { - node?: NodeResolver; - - cursor?: CursorResolver; - } - - export type NodeResolver = Resolver< - R, - Parent, - TContext - >; - export type CursorResolver< - R = CursorType, - Parent = UsersEdges, - TContext = SiemContext - > = Resolver; -} - -export namespace UsersNodeResolvers { - export interface Resolvers { - _id?: _IdResolver, TypeParent, TContext>; - - timestamp?: TimestampResolver, TypeParent, TContext>; - - user?: UserResolver, TypeParent, TContext>; - } - - export type _IdResolver, Parent = UsersNode, TContext = SiemContext> = Resolver< - R, - Parent, - TContext - >; - export type TimestampResolver< - R = Maybe, - Parent = UsersNode, - TContext = SiemContext - > = Resolver; - export type UserResolver< - R = Maybe, - Parent = UsersNode, - TContext = SiemContext - > = Resolver; -} - -export namespace UsersItemResolvers { - export interface Resolvers { - name?: NameResolver, TypeParent, TContext>; - - id?: IdResolver, TypeParent, TContext>; - - groupId?: GroupIdResolver, TypeParent, TContext>; - - groupName?: GroupNameResolver, TypeParent, TContext>; - - count?: CountResolver, TypeParent, TContext>; - } - - export type NameResolver< - R = Maybe, - Parent = UsersItem, - TContext = SiemContext - > = Resolver; - export type IdResolver< - R = Maybe, - Parent = UsersItem, - TContext = SiemContext - > = Resolver; - export type GroupIdResolver< - R = Maybe, - Parent = UsersItem, - TContext = SiemContext - > = Resolver; - export type GroupNameResolver< - R = Maybe, - Parent = UsersItem, - TContext = SiemContext - > = Resolver; - export type CountResolver< - R = Maybe, - Parent = UsersItem, - TContext = SiemContext - > = Resolver; -} - export namespace KpiNetworkDataResolvers { export interface Resolvers { networkEvents?: NetworkEventsResolver, TypeParent, TContext>; @@ -8815,14 +8428,6 @@ export type IResolvers = { CloudMachine?: CloudMachineResolvers.Resolvers; EndpointFields?: EndpointFieldsResolvers.Resolvers; FirstLastSeenHost?: FirstLastSeenHostResolvers.Resolvers; - IpOverviewData?: IpOverviewDataResolvers.Resolvers; - Overview?: OverviewResolvers.Resolvers; - AutonomousSystem?: AutonomousSystemResolvers.Resolvers; - AutonomousSystemOrganization?: AutonomousSystemOrganizationResolvers.Resolvers; - UsersData?: UsersDataResolvers.Resolvers; - UsersEdges?: UsersEdgesResolvers.Resolvers; - UsersNode?: UsersNodeResolvers.Resolvers; - UsersItem?: UsersItemResolvers.Resolvers; KpiNetworkData?: KpiNetworkDataResolvers.Resolvers; KpiNetworkHistogramData?: KpiNetworkHistogramDataResolvers.Resolvers; KpiHostsData?: KpiHostsDataResolvers.Resolvers; diff --git a/x-pack/plugins/security_solution/server/init_server.ts b/x-pack/plugins/security_solution/server/init_server.ts index ac0273ec1770d..3d2833f1c6c60 100644 --- a/x-pack/plugins/security_solution/server/init_server.ts +++ b/x-pack/plugins/security_solution/server/init_server.ts @@ -10,7 +10,6 @@ import { createAuthenticationsResolvers } from './graphql/authentications'; import { createScalarToStringArrayValueResolvers } from './graphql/ecs'; import { createEsValueResolvers, createEventsResolvers } from './graphql/events'; import { createHostsResolvers } from './graphql/hosts'; -import { createIpDetailsResolvers } from './graphql/ip_details'; import { createKpiHostsResolvers } from './graphql/kpi_hosts'; import { createKpiNetworkResolvers } from './graphql/kpi_network'; import { createNetworkResolvers } from './graphql/network'; @@ -35,7 +34,6 @@ export const initServer = (libs: AppBackendLibs) => { createEsValueResolvers() as IResolvers, createEventsResolvers(libs) as IResolvers, createHostsResolvers(libs) as IResolvers, - createIpDetailsResolvers(libs) as IResolvers, createKpiNetworkResolvers(libs) as IResolvers, createMatrixHistogramResolvers(libs) as IResolvers, createNoteResolvers(libs) as IResolvers, diff --git a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts index 3bfb3d9492353..6348ee930a109 100644 --- a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts +++ b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts @@ -16,7 +16,6 @@ import { KpiHosts } from '../kpi_hosts'; import { ElasticsearchKpiHostsAdapter } from '../kpi_hosts/elasticsearch_adapter'; import { ElasticsearchIndexFieldAdapter, IndexFields } from '../index_fields'; -import { ElasticsearchIpDetailsAdapter, IpDetails } from '../ip_details'; import { KpiNetwork } from '../kpi_network'; import { ElasticsearchKpiNetworkAdapter } from '../kpi_network/elasticsearch_adapter'; @@ -45,7 +44,6 @@ export function compose( events: new Events(new ElasticsearchEventsAdapter(framework)), fields: new IndexFields(new ElasticsearchIndexFieldAdapter()), hosts: new Hosts(new ElasticsearchHostsAdapter(framework, endpointContext)), - ipDetails: new IpDetails(new ElasticsearchIpDetailsAdapter(framework)), kpiHosts: new KpiHosts(new ElasticsearchKpiHostsAdapter(framework)), kpiNetwork: new KpiNetwork(new ElasticsearchKpiNetworkAdapter(framework)), matrixHistogram: new MatrixHistogram(new ElasticsearchMatrixHistogramAdapter(framework)), diff --git a/x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.test.ts b/x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.test.ts deleted file mode 100644 index 6249e60d9a2be..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.test.ts +++ /dev/null @@ -1,53 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import { FlowTarget } from '../../graphql/types'; - -import { getIpOverviewAgg, getIpOverviewHostAgg, getUsersEdges } from './elasticsearch_adapter'; - -import { - formattedDestination, - formattedEmptySource, - formattedHost, - formattedSource, - mockFormattedUsersEdges, - mockUsersData, - responseAggs, -} from './mock'; - -describe('elasticsearch_adapter', () => { - describe('#getIpOverview', () => { - test('will return a destination correctly', () => { - const destination = getIpOverviewAgg( - FlowTarget.destination, - responseAggs.aggregations.destination! - ); - expect(destination).toEqual(formattedDestination); - }); - - test('will return a source correctly', () => { - const source = getIpOverviewAgg(FlowTarget.source, responseAggs.aggregations.source!); - expect(source).toEqual(formattedSource); - }); - - test('will return a host correctly', () => { - const host = getIpOverviewHostAgg(responseAggs.aggregations.host); - expect(host).toEqual(formattedHost); - }); - - test('will return an empty source correctly', () => { - const source = getIpOverviewAgg(FlowTarget.source, {}); - expect(source).toEqual(formattedEmptySource); - }); - }); - - describe('#getUsers', () => { - test('will format edges correctly', () => { - // @ts-expect-error Re-work `DatabaseSearchResponse` types as mock ES Response won't match - const edges = getUsersEdges(mockUsersData); - expect(edges).toEqual(mockFormattedUsersEdges); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.ts deleted file mode 100644 index 90803ca302bd4..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/ip_details/elasticsearch_adapter.ts +++ /dev/null @@ -1,160 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get, getOr } from 'lodash/fp'; - -import { - AutonomousSystem, - GeoEcsFields, - HostEcsFields, - IpOverviewData, - UsersData, - UsersEdges, -} from '../../graphql/types'; -import { inspectStringifyObject } from '../../utils/build_query'; -import { DatabaseSearchResponse, FrameworkAdapter, FrameworkRequest } from '../framework'; -import { TermAggregation } from '../types'; -import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; -import { IpOverviewRequestOptions, UsersRequestOptions } from './index'; -import { buildOverviewQuery } from './query_overview.dsl'; -import { buildUsersQuery } from './query_users.dsl'; - -import { - IpDetailsAdapter, - IpOverviewHit, - OverviewHit, - OverviewHostHit, - UsersBucketsItem, -} from './types'; - -export class ElasticsearchIpDetailsAdapter implements IpDetailsAdapter { - constructor(private readonly framework: FrameworkAdapter) {} - - public async getIpDetails( - request: FrameworkRequest, - options: IpOverviewRequestOptions - ): Promise { - const dsl = buildOverviewQuery(options); - const response = await this.framework.callWithRequest( - request, - 'search', - dsl - ); - - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - - return { - inspect, - ...getIpOverviewAgg('source', getOr({}, 'aggregations.source', response)), - ...getIpOverviewAgg('destination', getOr({}, 'aggregations.destination', response)), - ...getIpOverviewHostAgg(getOr({}, 'aggregations.host', response)), - }; - } - - public async getUsers( - request: FrameworkRequest, - options: UsersRequestOptions - ): Promise { - if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { - throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); - } - const dsl = buildUsersQuery(options); - const response = await this.framework.callWithRequest( - request, - 'search', - dsl - ); - - const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; - const totalCount = getOr(0, 'aggregations.user_count.value', response); - const usersEdges = getUsersEdges(response); - const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; - const edges = usersEdges.splice(cursorStart, querySize - cursorStart); - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - const showMorePagesIndicator = totalCount > fakeTotalCount; - return { - edges, - inspect, - pageInfo: { - activePage: activePage ? activePage : 0, - fakeTotalCount, - showMorePagesIndicator, - }, - totalCount, - }; - } -} - -export const getIpOverviewAgg = (type: string, overviewHit: OverviewHit | {}) => { - const firstSeen = getOr(null, `firstSeen.value_as_string`, overviewHit); - const lastSeen = getOr(null, `lastSeen.value_as_string`, overviewHit); - const autonomousSystem: AutonomousSystem | null = getOr( - null, - `as.results.hits.hits[0]._source.${type}.as`, - overviewHit - ); - const geoFields: GeoEcsFields | null = getOr( - null, - `geo.results.hits.hits[0]._source.${type}.geo`, - overviewHit - ); - - return { - [type]: { - firstSeen, - lastSeen, - autonomousSystem: { - ...autonomousSystem, - }, - geo: { - ...geoFields, - }, - }, - }; -}; - -export const getIpOverviewHostAgg = (overviewHostHit: OverviewHostHit | {}) => { - const hostFields: HostEcsFields | null = getOr( - null, - `results.hits.hits[0]._source.host`, - overviewHostHit - ); - return { - host: { - ...hostFields, - }, - }; -}; - -export const getUsersEdges = ( - response: DatabaseSearchResponse -): UsersEdges[] => - getOr([], `aggregations.users.buckets`, response).map((bucket: UsersBucketsItem) => ({ - node: { - _id: bucket.key, - user: { - id: getOr([], 'id.buckets', bucket).map((id: UsersBucketsItem) => id.key), - name: bucket.key, - groupId: getOr([], 'groupId.buckets', bucket).map( - (groupId: UsersBucketsItem) => groupId.key - ), - groupName: getOr([], 'groupName.buckets', bucket).map( - (groupName: UsersBucketsItem) => groupName.key - ), - count: get('doc_count', bucket), - }, - }, - cursor: { - value: bucket.key, - tiebreaker: null, - }, - })); diff --git a/x-pack/plugins/security_solution/server/lib/ip_details/index.ts b/x-pack/plugins/security_solution/server/lib/ip_details/index.ts deleted file mode 100644 index ed8824bc284e4..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/ip_details/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FlowTarget, IpOverviewData, UsersData, UsersSortField } from '../../graphql/types'; -import { FrameworkRequest, RequestOptions, RequestOptionsPaginated } from '../framework'; - -import { IpDetailsAdapter } from './types'; - -export * from './elasticsearch_adapter'; - -export interface IpOverviewRequestOptions extends RequestOptions { - ip: string; -} - -export interface UsersRequestOptions extends RequestOptionsPaginated { - ip: string; - sort: UsersSortField; - flowTarget: FlowTarget; -} - -export class IpDetails { - constructor(private readonly adapter: IpDetailsAdapter) {} - - public async getIpOverview( - req: FrameworkRequest, - options: IpOverviewRequestOptions - ): Promise { - return this.adapter.getIpDetails(req, options); - } - - public async getUsers(req: FrameworkRequest, options: UsersRequestOptions): Promise { - return this.adapter.getUsers(req, options); - } -} diff --git a/x-pack/plugins/security_solution/server/lib/ip_details/mock.ts b/x-pack/plugins/security_solution/server/lib/ip_details/mock.ts deleted file mode 100644 index 1db86e7766fcf..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/ip_details/mock.ts +++ /dev/null @@ -1,430 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { UsersEdges } from '../../graphql/types'; - -import { IpOverviewHit, UsersResponse } from './types'; - -export const responseAggs: IpOverviewHit = { - aggregations: { - destination: { - doc_count: 882307, - geo: { - doc_count: 62089, - results: { - hits: { - total: { - value: 62089, - relation: 'eq', - }, - max_score: null, - hits: [ - { - _source: { - destination: { - geo: { - continent_name: 'Asia', - region_iso_code: 'IN-KA', - city_name: 'Bengaluru', - country_iso_code: 'IN', - region_name: 'Karnataka', - location: { - lon: 77.5833, - lat: 12.9833, - }, - }, - }, - }, - sort: [1553894176003], - }, - ], - }, - }, - }, - lastSeen: { - value: 1553900180003, - value_as_string: '2019-03-29T22:56:20.003Z', - }, - firstSeen: { - value: 1551388820000, - value_as_string: '2019-02-28T21:20:20.000Z', - }, - autonomousSystem: { - doc_count: 0, - results: { - hits: { - total: { - value: 0, - relation: 'eq', - }, - max_score: null, - hits: [], - }, - }, - }, - }, - source: { - doc_count: 1002234, - geo: { - doc_count: 1507, - results: { - hits: { - total: { - value: 1507, - relation: 'eq', - }, - max_score: null, - hits: [ - { - _index: 'filebeat-8.0.0-2019.03.21-000002', - _type: '_doc', - _id: 'dHQ6y2kBCQofM5eXi5OE', - _score: null, - _source: { - source: { - geo: { - continent_name: 'Asia', - region_iso_code: 'IN-KA', - city_name: 'Bengaluru', - country_iso_code: 'IN', - region_name: 'Karnataka', - location: { - lon: 77.5833, - lat: 12.9833, - }, - }, - }, - }, - sort: [1553892804003], - }, - ], - }, - }, - }, - lastSeen: { - value: 1553900180003, - value_as_string: '2019-03-29T22:56:20.003Z', - }, - firstSeen: { - value: 1551388804322, - value_as_string: '2019-02-28T21:20:04.322Z', - }, - autonomousSystem: { - doc_count: 0, - results: { - hits: { - total: { - value: 0, - relation: 'eq', - }, - max_score: null, - hits: [], - }, - }, - }, - }, - host: { - doc_count: 1588091, - results: { - hits: { - total: { - value: 1588091, - relation: 'eq', - }, - max_score: null, - hits: [ - { - _index: 'filebeat-8.0.0-2019.05.20-000004', - _type: '_doc', - _id: 'NU9dD2sB9v5HJNSHMMRc', - _score: null, - _source: { - host: { - hostname: 'suricata-iowa', - os: { - kernel: '4.15.0-1032-gcp', - codename: 'bionic', - name: 'Ubuntu', - family: 'debian', - version: '18.04.2 LTS (Bionic Beaver)', - platform: 'ubuntu', - }, - ip: ['10.128.0.4', 'fe80::4001:aff:fe80:4'], - containerized: false, - name: 'suricata-iowa', - id: 'be1f3d767896212736b880e846876dcb', - mac: ['42:01:0a:80:00:04'], - architecture: 'x86_64', - }, - }, - sort: [1559330892000], - }, - ], - }, - }, - }, - }, - _shards: { - total: 42, - successful: 42, - skipped: 0, - failed: 0, - }, - hits: { - total: { - value: 71358841, - relation: 'eq', - }, - max_score: null, - hits: [], - }, - took: 392, - timeout: 500, -}; - -export const formattedDestination = { - destination: { - firstSeen: '2019-02-28T21:20:20.000Z', - lastSeen: '2019-03-29T22:56:20.003Z', - autonomousSystem: {}, - geo: { - continent_name: 'Asia', - region_iso_code: 'IN-KA', - city_name: 'Bengaluru', - country_iso_code: 'IN', - region_name: 'Karnataka', - location: { - lon: 77.5833, - lat: 12.9833, - }, - }, - }, -}; - -export const formattedSource = { - source: { - firstSeen: '2019-02-28T21:20:04.322Z', - lastSeen: '2019-03-29T22:56:20.003Z', - autonomousSystem: {}, - geo: { - continent_name: 'Asia', - region_iso_code: 'IN-KA', - city_name: 'Bengaluru', - country_iso_code: 'IN', - region_name: 'Karnataka', - location: { - lon: 77.5833, - lat: 12.9833, - }, - }, - }, -}; - -export const formattedHost = { - host: { - hostname: 'suricata-iowa', - os: { - kernel: '4.15.0-1032-gcp', - codename: 'bionic', - name: 'Ubuntu', - family: 'debian', - version: '18.04.2 LTS (Bionic Beaver)', - platform: 'ubuntu', - }, - ip: ['10.128.0.4', 'fe80::4001:aff:fe80:4'], - containerized: false, - name: 'suricata-iowa', - id: 'be1f3d767896212736b880e846876dcb', - mac: ['42:01:0a:80:00:04'], - architecture: 'x86_64', - }, -}; - -export const formattedEmptySource = { - source: { - firstSeen: null, - lastSeen: null, - autonomousSystem: {}, - geo: {}, - }, -}; - -export const mockUsersData: UsersResponse = { - took: 445, - timed_out: false, - _shards: { - total: 59, - successful: 59, - skipped: 0, - failed: 0, - }, - hits: { - max_score: null, - hits: [], - }, - aggregations: { - user_count: { - value: 3, - }, - users: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: '_apt', - doc_count: 10, - groupName: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'nogroup', - doc_count: 10, - }, - ], - }, - groupId: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: '65534', - doc_count: 10, - }, - ], - }, - id: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: '104', - doc_count: 10, - }, - ], - }, - }, - { - key: 'root', - doc_count: 109, - groupName: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'Debian-exim', - doc_count: 72, - }, - { - key: 'root', - doc_count: 37, - }, - ], - }, - groupId: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: '116', - doc_count: 72, - }, - { - key: '0', - doc_count: 37, - }, - ], - }, - id: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: '0', - doc_count: 109, - }, - ], - }, - }, - { - key: 'systemd-resolve', - doc_count: 4, - groupName: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [], - }, - groupId: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [], - }, - id: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: '102', - doc_count: 4, - }, - ], - }, - }, - ], - }, - }, -}; - -export const mockFormattedUsersEdges: UsersEdges[] = [ - { - node: { - _id: '_apt', - user: { - id: ['104'], - name: '_apt', - groupId: ['65534'], - groupName: ['nogroup'], - count: 10, - }, - }, - cursor: { - value: '_apt', - tiebreaker: null, - }, - }, - { - node: { - _id: 'root', - user: { - id: ['0'], - name: 'root', - groupId: ['116', '0'], - groupName: ['Debian-exim', 'root'], - count: 109, - }, - }, - cursor: { - value: 'root', - tiebreaker: null, - }, - }, - { - node: { - _id: 'systemd-resolve', - user: { - id: ['102'], - name: 'systemd-resolve', - groupId: [], - groupName: [], - count: 4, - }, - }, - cursor: { - value: 'systemd-resolve', - tiebreaker: null, - }, - }, -]; diff --git a/x-pack/plugins/security_solution/server/lib/ip_details/query_overview.dsl.ts b/x-pack/plugins/security_solution/server/lib/ip_details/query_overview.dsl.ts deleted file mode 100644 index d9c8f32d0b465..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/ip_details/query_overview.dsl.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty } from 'lodash/fp'; -import { IpOverviewRequestOptions } from './index'; - -const getAggs = (type: string, ip: string) => { - return { - [type]: { - filter: { - term: { - [`${type}.ip`]: ip, - }, - }, - aggs: { - firstSeen: { - min: { - field: '@timestamp', - }, - }, - lastSeen: { - max: { - field: '@timestamp', - }, - }, - as: { - filter: { - exists: { - field: `${type}.as`, - }, - }, - aggs: { - results: { - top_hits: { - size: 1, - _source: [`${type}.as`], - sort: [ - { - '@timestamp': 'desc', - }, - ], - }, - }, - }, - }, - geo: { - filter: { - exists: { - field: `${type}.geo`, - }, - }, - aggs: { - results: { - top_hits: { - size: 1, - _source: [`${type}.geo`], - sort: [ - { - '@timestamp': 'desc', - }, - ], - }, - }, - }, - }, - }, - }, - }; -}; - -const getHostAggs = (ip: string) => { - return { - host: { - filter: { - term: { - 'host.ip': ip, - }, - }, - aggs: { - results: { - top_hits: { - size: 1, - _source: ['host'], - sort: [ - { - '@timestamp': 'desc', - }, - ], - }, - }, - }, - }, - }; -}; - -export const buildOverviewQuery = ({ - defaultIndex, - docValueFields, - ip, -}: IpOverviewRequestOptions) => { - const dslQuery = { - allowNoIndices: true, - index: defaultIndex, - ignoreUnavailable: true, - body: { - ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), - aggs: { - ...getAggs('source', ip), - ...getAggs('destination', ip), - ...getHostAggs(ip), - }, - query: { - bool: { - should: [], - }, - }, - size: 0, - track_total_hits: false, - }, - }; - - return dslQuery; -}; diff --git a/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts b/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts deleted file mode 100644 index 293a487777fd2..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/ip_details/query_users.dsl.ts +++ /dev/null @@ -1,104 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { assertUnreachable } from '../../../common/utility_types'; -import { Direction, UsersFields, UsersSortField } from '../../graphql/types'; -import { createQueryFilterClauses } from '../../utils/build_query'; - -import { UsersRequestOptions } from './index'; - -export const buildUsersQuery = ({ - ip, - sort, - filterQuery, - flowTarget, - pagination: { querySize }, - defaultIndex, - sourceConfiguration: { - fields: { timestamp }, - }, - timerange: { from, to }, -}: UsersRequestOptions) => { - const filter = [ - ...createQueryFilterClauses(filterQuery), - { - range: { - [timestamp]: { gte: from, lte: to, format: 'strict_date_optional_time' }, - }, - }, - { term: { [`${flowTarget}.ip`]: ip } }, - ]; - - const dslQuery = { - allowNoIndices: true, - index: defaultIndex, - ignoreUnavailable: true, - body: { - aggs: { - user_count: { - cardinality: { - field: 'user.name', - }, - }, - users: { - terms: { - field: 'user.name', - size: querySize, - order: { - ...getQueryOrder(sort), - }, - }, - aggs: { - id: { - terms: { - field: 'user.id', - }, - }, - groupId: { - terms: { - field: 'user.group.id', - }, - }, - groupName: { - terms: { - field: 'user.group.name', - }, - }, - }, - }, - }, - query: { - bool: { - filter, - must_not: [ - { - term: { - 'event.category': 'authentication', - }, - }, - ], - }, - }, - size: 0, - track_total_hits: false, - }, - }; - - return dslQuery; -}; - -type QueryOrder = { _count: Direction } | { _key: Direction }; - -const getQueryOrder = (sort: UsersSortField): QueryOrder => { - switch (sort.field) { - case UsersFields.name: - return { _key: sort.direction }; - case UsersFields.count: - return { _count: sort.direction }; - default: - return assertUnreachable(sort.field); - } -}; diff --git a/x-pack/plugins/security_solution/server/lib/ip_details/types.ts b/x-pack/plugins/security_solution/server/lib/ip_details/types.ts deleted file mode 100644 index d137d919932f7..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/ip_details/types.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { IpOverviewData, UsersData } from '../../graphql/types'; -import { FrameworkRequest, RequestBasicOptions } from '../framework'; -import { Hit, ShardsResponse, TotalValue } from '../types'; - -export interface IpDetailsAdapter { - getIpDetails(request: FrameworkRequest, options: RequestBasicOptions): Promise; - getUsers(request: FrameworkRequest, options: RequestBasicOptions): Promise; -} - -interface ResultHit { - doc_count: number; - results: { - hits: { - total: TotalValue | number; - max_score: number | null; - hits: Array<{ - _source: T; - sort?: [number]; - _index?: string; - _type?: string; - _id?: string; - _score?: number | null; - }>; - }; - }; -} - -export interface OverviewHit { - took?: number; - timed_out?: boolean; - _scroll_id?: string; - _shards?: ShardsResponse; - timeout?: number; - hits?: { - total: number; - hits: Hit[]; - }; - doc_count: number; - geo: ResultHit; - autonomousSystem: ResultHit; - firstSeen: { - value: number; - value_as_string: string; - }; - lastSeen: { - value: number; - value_as_string: string; - }; -} - -export type OverviewHostHit = ResultHit; - -export interface IpOverviewHit { - aggregations: { - destination?: OverviewHit; - source?: OverviewHit; - host: ResultHit; - }; - _shards: { - total: number; - successful: number; - skipped: number; - failed: number; - }; - hits: { - total: { - value: number; - relation: string; - }; - max_score: number | null; - hits: []; - }; - took: number; - timeout: number; -} - -// Users Table - -export interface UsersResponse { - took: number; - timed_out: boolean; - _shards: UsersShards; - hits: UsersHits; - aggregations: Aggregations; -} -interface UsersShards { - total: number; - successful: number; - skipped: number; - failed: number; -} -interface UsersHits { - max_score: null; - hits: string[]; -} -interface Aggregations { - user_count: UserCount; - users: Users; -} -interface UserCount { - value: number; -} -interface Users { - doc_count_error_upper_bound: number; - sum_other_doc_count: number; - buckets: UsersBucketsItem[]; -} -export interface UsersBucketsItem { - key: string; - doc_count: number; - groupName?: UsersGroupName; - groupId?: UsersGroupId; - id?: Id; -} -export interface UsersGroupName { - doc_count_error_upper_bound: number; - sum_other_doc_count: number; - buckets: UsersBucketsItem[]; -} -export interface UsersGroupId { - doc_count_error_upper_bound: number; - sum_other_doc_count: number; - buckets: UsersBucketsItem[]; -} -interface Id { - doc_count_error_upper_bound: number; - sum_other_doc_count: number; - buckets: UsersBucketsItem[]; -} diff --git a/x-pack/plugins/security_solution/server/lib/types.ts b/x-pack/plugins/security_solution/server/lib/types.ts index 3c7c1cd3d7cff..6e233f6e49d3b 100644 --- a/x-pack/plugins/security_solution/server/lib/types.ts +++ b/x-pack/plugins/security_solution/server/lib/types.ts @@ -13,7 +13,6 @@ import { Events } from './events'; import { FrameworkAdapter, FrameworkRequest } from './framework'; import { Hosts } from './hosts'; import { IndexFields } from './index_fields'; -import { IpDetails } from './ip_details'; import { KpiHosts } from './kpi_hosts'; import { KpiNetwork } from './kpi_network'; import { Network } from './network'; @@ -31,7 +30,6 @@ export interface AppDomainLibs { events: Events; fields: IndexFields; hosts: Hosts; - ipDetails: IpDetails; matrixHistogram: MatrixHistogram; network: Network; kpiNetwork: KpiNetwork; diff --git a/x-pack/test/api_integration/apis/security_solution/index.js b/x-pack/test/api_integration/apis/security_solution/index.js index a9ddf091245f7..a143d94dde172 100644 --- a/x-pack/test/api_integration/apis/security_solution/index.js +++ b/x-pack/test/api_integration/apis/security_solution/index.js @@ -21,7 +21,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./timeline')); loadTestFile(require.resolve('./timeline_details')); // loadTestFile(require.resolve('./uncommon_processes')); - loadTestFile(require.resolve('./users')); + // loadTestFile(require.resolve('./users')); // loadTestFile(require.resolve('./tls')); loadTestFile(require.resolve('./feature_controls')); }); diff --git a/x-pack/test/api_integration/apis/security_solution/network_details.ts b/x-pack/test/api_integration/apis/security_solution/network_details.ts index cffcd790fa19c..7b851e875454d 100644 --- a/x-pack/test/api_integration/apis/security_solution/network_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/network_details.ts @@ -5,7 +5,9 @@ */ import expect from '@kbn/expect'; +// @ts-expect-error import { ipOverviewQuery } from '../../../../plugins/security_solution/public/network/containers/details/index.gql_query'; +// @ts-expect-error import { GetIpOverviewQuery } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/apis/security_solution/users.ts b/x-pack/test/api_integration/apis/security_solution/users.ts index abb2c5b2f5bbd..9d42fc0b9788b 100644 --- a/x-pack/test/api_integration/apis/security_solution/users.ts +++ b/x-pack/test/api_integration/apis/security_solution/users.ts @@ -5,11 +5,14 @@ */ import expect from '@kbn/expect'; +// @ts-expect-error import { usersQuery } from '../../../../plugins/security_solution/public/network/containers/users/index.gql_query'; import { Direction, + // @ts-expect-error UsersFields, FlowTarget, + // @ts-expect-error GetUsersQuery, } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; From 839817ca9a7a359ec17ab067cc360c3cff30e3e5 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 24 Sep 2020 09:55:32 +0200 Subject: [PATCH 83/92] [Lens] show meta field data in Lens (#77210) --- .../indexpattern_datasource/datapanel.scss | 21 -- .../datapanel.test.tsx | 35 +- .../indexpattern_datasource/datapanel.tsx | 318 ++++++------------ .../dimension_panel/field_select.tsx | 11 +- .../indexpattern_datasource/field_item.tsx | 5 +- .../indexpattern_datasource/field_list.scss | 20 ++ .../indexpattern_datasource/field_list.tsx | 193 +++++++++++ .../fields_accordion.test.tsx | 10 +- .../fields_accordion.tsx | 4 +- .../indexpattern_datasource/loader.test.ts | 52 ++- .../public/indexpattern_datasource/loader.ts | 1 + .../public/indexpattern_datasource/types.ts | 1 + .../server/routes/existing_fields.test.ts | 32 +- .../lens/server/routes/existing_fields.ts | 32 +- .../apis/lens/existing_fields.ts | 9 + 15 files changed, 493 insertions(+), 251 deletions(-) create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/field_list.scss create mode 100644 x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.scss b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.scss index 70fb57ee79ee5..155b954e9cf17 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.scss +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.scss @@ -10,27 +10,6 @@ margin-bottom: $euiSizeS; } -/** - * 1. Don't cut off the shadow of the field items - */ - -.lnsInnerIndexPatternDataPanel__listWrapper { - @include euiOverflowShadow; - @include euiScrollBar; - margin-left: -$euiSize; /* 1 */ - position: relative; - flex-grow: 1; - overflow: auto; -} - -.lnsInnerIndexPatternDataPanel__list { - padding-top: $euiSizeS; - position: absolute; - top: 0; - left: $euiSize; /* 1 */ - right: $euiSizeXS; /* 1 */ -} - .lnsInnerIndexPatternDataPanel__fieldItems { // Quick fix for making sure the shadow and focus rings are visible outside the accordion bounds padding: $euiSizeXS $euiSizeXS 0; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index f17bf172b0fb1..7fb64d1613d32 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -623,11 +623,40 @@ describe('IndexPattern Data Panel', () => { ).toEqual(['client', 'source', 'timestampLabel']); }); + it('should show meta fields accordion', async () => { + const wrapper = mountWithIntl( + + ); + wrapper + .find('[data-test-subj="lnsIndexPatternMetaFields"]') + .find('button') + .first() + .simulate('click'); + expect( + wrapper + .find('[data-test-subj="lnsIndexPatternMetaFields"]') + .find(FieldItem) + .first() + .prop('field').name + ).toEqual('_id'); + }); + it('should display NoFieldsCallout when all fields are empty', async () => { const wrapper = mountWithIntl( ); - expect(wrapper.find(NoFieldsCallout).length).toEqual(1); + expect(wrapper.find(NoFieldsCallout).length).toEqual(2); expect( wrapper .find('[data-test-subj="lnsIndexPatternAvailableFields"]') @@ -654,7 +683,7 @@ describe('IndexPattern Data Panel', () => { .length ).toEqual(1); wrapper.setProps({ existingFields: { idx1: {} } }); - expect(wrapper.find(NoFieldsCallout).length).toEqual(1); + expect(wrapper.find(NoFieldsCallout).length).toEqual(2); }); it('should filter down by name', () => { @@ -699,7 +728,7 @@ describe('IndexPattern Data Panel', () => { expect(wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)).toEqual([ 'Records', ]); - expect(wrapper.find(NoFieldsCallout).length).toEqual(2); + expect(wrapper.find(NoFieldsCallout).length).toEqual(3); }); it('should toggle type if clicked again', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx index f7adf91e307da..4e85cb5b5d46c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -5,14 +5,13 @@ */ import './datapanel.scss'; -import { uniq, keyBy, groupBy, throttle } from 'lodash'; -import React, { useState, useEffect, memo, useCallback, useMemo } from 'react'; +import { uniq, keyBy, groupBy } from 'lodash'; +import React, { useState, memo, useCallback, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiContextMenuPanel, EuiContextMenuItem, - EuiContextMenuPanelProps, EuiPopover, EuiCallOut, EuiFormControlLayout, @@ -25,8 +24,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { DataPublicPluginStart, EsQueryConfig, Query, Filter } from 'src/plugins/data/public'; import { DatasourceDataPanelProps, DataType, StateSetter } from '../types'; import { ChildDragDropProvider, DragContextState } from '../drag_drop'; -import { FieldItem } from './field_item'; -import { NoFieldsCallout } from './no_fields_callout'; import { IndexPattern, IndexPatternPrivateState, @@ -37,7 +34,6 @@ import { trackUiEvent } from '../lens_ui_telemetry'; import { syncExistingFields } from './loader'; import { fieldExists } from './pure_helpers'; import { Loader } from '../loader'; -import { FieldsAccordion } from './fields_accordion'; import { esQuery, IIndexPattern } from '../../../../../src/plugins/data/public'; export type Props = DatasourceDataPanelProps & { @@ -52,18 +48,13 @@ export type Props = DatasourceDataPanelProps & { import { LensFieldIcon } from './lens_field_icon'; import { ChangeIndexPattern } from './change_indexpattern'; import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; - -// TODO the typings for EuiContextMenuPanel are incorrect - watchedItemProps is missing. This can be removed when the types are adjusted -const FixedEuiContextMenuPanel = (EuiContextMenuPanel as unknown) as React.FunctionComponent< - EuiContextMenuPanelProps & { watchedItemProps: string[] } ->; +import { FieldGroups, FieldList } from './field_list'; function sortFields(fieldA: IndexPatternField, fieldB: IndexPatternField) { return fieldA.displayName.localeCompare(fieldB.displayName, undefined, { sensitivity: 'base' }); } const supportedFieldTypes = new Set(['string', 'number', 'boolean', 'date', 'ip', 'document']); -const PAGINATION_SIZE = 50; const fieldTypeNames: Record = { document: i18n.translate('xpack.lens.datatypes.record', { defaultMessage: 'record' }), @@ -212,18 +203,19 @@ interface DataPanelState { isTypeFilterOpen: boolean; isAvailableAccordionOpen: boolean; isEmptyAccordionOpen: boolean; + isMetaAccordionOpen: boolean; } -export interface FieldsGroup { +const defaultFieldGroups: { specialFields: IndexPatternField[]; availableFields: IndexPatternField[]; emptyFields: IndexPatternField[]; -} - -const defaultFieldGroups = { + metaFields: IndexPatternField[]; +} = { specialFields: [], availableFields: [], emptyFields: [], + metaFields: [], }; const fieldFiltersLabel = i18n.translate('xpack.lens.indexPatterns.fieldFiltersLabel', { @@ -261,9 +253,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ isTypeFilterOpen: false, isAvailableAccordionOpen: true, isEmptyAccordionOpen: false, + isMetaAccordionOpen: false, }); - const [pageSize, setPageSize] = useState(PAGINATION_SIZE); - const [scrollContainer, setScrollContainer] = useState(undefined); const currentIndexPattern = indexPatterns[currentIndexPatternId]; const allFields = currentIndexPattern.fields; const clearLocalState = () => setLocalState((s) => ({ ...s, nameFilter: '', typeFilter: [] })); @@ -272,17 +263,11 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ (type) => type in fieldTypeNames ); - useEffect(() => { - // Reset the scroll if we have made material changes to the field list - if (scrollContainer) { - scrollContainer.scrollTop = 0; - setPageSize(PAGINATION_SIZE); - } - }, [localState.nameFilter, localState.typeFilter, currentIndexPatternId, scrollContainer]); + const fieldInfoUnavailable = existenceFetchFailed || currentIndexPattern.hasRestrictions; - const fieldGroups: FieldsGroup = useMemo(() => { + const unfilteredFieldGroups: FieldGroups = useMemo(() => { + const fieldByName = keyBy(allFields, 'name'); const containsData = (field: IndexPatternField) => { - const fieldByName = keyBy(allFields, 'name'); const overallField = fieldByName[field.name]; return ( @@ -294,32 +279,105 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ supportedFieldTypes.has(field.type) ); const sorted = allSupportedTypesFields.sort(sortFields); + let groupedFields; // optimization before existingFields are synced if (!hasSyncedExistingFields) { - return { + groupedFields = { ...defaultFieldGroups, ...groupBy(sorted, (field) => { if (field.type === 'document') { return 'specialFields'; + } else if (field.meta) { + return 'metaFields'; } else { return 'emptyFields'; } }), }; } - return { + groupedFields = { ...defaultFieldGroups, ...groupBy(sorted, (field) => { if (field.type === 'document') { return 'specialFields'; + } else if (field.meta) { + return 'metaFields'; } else if (containsData(field)) { return 'availableFields'; } else return 'emptyFields'; }), }; - }, [allFields, existingFields, currentIndexPattern, hasSyncedExistingFields]); - const filteredFieldGroups: FieldsGroup = useMemo(() => { + const fieldGroupDefinitions: FieldGroups = { + SpecialFields: { + fields: groupedFields.specialFields, + fieldCount: 1, + isAffectedByGlobalFilter: false, + isAffectedByTimeFilter: false, + isInitiallyOpen: false, + showInAccordion: false, + title: '', + hideDetails: true, + }, + AvailableFields: { + fields: groupedFields.availableFields, + fieldCount: groupedFields.availableFields.length, + isInitiallyOpen: true, + showInAccordion: true, + title: fieldInfoUnavailable + ? i18n.translate('xpack.lens.indexPattern.allFieldsLabel', { + defaultMessage: 'All fields', + }) + : i18n.translate('xpack.lens.indexPattern.availableFieldsLabel', { + defaultMessage: 'Available fields', + }), + + isAffectedByGlobalFilter: !!filters.length, + isAffectedByTimeFilter: true, + hideDetails: fieldInfoUnavailable, + }, + EmptyFields: { + fields: groupedFields.emptyFields, + fieldCount: groupedFields.emptyFields.length, + isAffectedByGlobalFilter: false, + isAffectedByTimeFilter: false, + isInitiallyOpen: false, + showInAccordion: true, + hideDetails: false, + title: i18n.translate('xpack.lens.indexPattern.emptyFieldsLabel', { + defaultMessage: 'Empty fields', + }), + }, + MetaFields: { + fields: groupedFields.metaFields, + fieldCount: groupedFields.metaFields.length, + isAffectedByGlobalFilter: false, + isAffectedByTimeFilter: false, + isInitiallyOpen: false, + showInAccordion: true, + hideDetails: false, + title: i18n.translate('xpack.lens.indexPattern.metaFieldsLabel', { + defaultMessage: 'Meta fields', + }), + }, + }; + + // do not show empty field accordion if there is no existence information + if (fieldInfoUnavailable) { + delete fieldGroupDefinitions.EmptyFields; + } + + return fieldGroupDefinitions; + }, [ + allFields, + existingFields, + currentIndexPattern, + hasSyncedExistingFields, + fieldInfoUnavailable, + filters.length, + ]); + + const fieldGroups: FieldGroups = useMemo(() => { const filterFieldGroup = (fieldGroup: IndexPatternField[]) => fieldGroup.filter((field) => { if ( @@ -329,76 +387,18 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ ) { return false; } - if (localState.typeFilter.length > 0) { return localState.typeFilter.includes(field.type as DataType); } return true; }); - - return Object.entries(fieldGroups).reduce((acc, [name, fields]) => { - return { - ...acc, - [name]: filterFieldGroup(fields), - }; - }, defaultFieldGroups); - }, [fieldGroups, localState.nameFilter, localState.typeFilter]); - - const lazyScroll = useCallback(() => { - if (scrollContainer) { - const nearBottom = - scrollContainer.scrollTop + scrollContainer.clientHeight > - scrollContainer.scrollHeight * 0.9; - if (nearBottom) { - const displayedFieldsLength = - (localState.isAvailableAccordionOpen ? filteredFieldGroups.availableFields.length : 0) + - (localState.isEmptyAccordionOpen ? filteredFieldGroups.emptyFields.length : 0); - setPageSize( - Math.max( - PAGINATION_SIZE, - Math.min(pageSize + PAGINATION_SIZE * 0.5, displayedFieldsLength) - ) - ); - } - } - }, [ - scrollContainer, - localState.isAvailableAccordionOpen, - localState.isEmptyAccordionOpen, - filteredFieldGroups, - pageSize, - setPageSize, - ]); - - const [paginatedAvailableFields, paginatedEmptyFields]: [ - IndexPatternField[], - IndexPatternField[] - ] = useMemo(() => { - const { availableFields, emptyFields } = filteredFieldGroups; - const isAvailableAccordionOpen = localState.isAvailableAccordionOpen; - const isEmptyAccordionOpen = localState.isEmptyAccordionOpen; - - if (isAvailableAccordionOpen && isEmptyAccordionOpen) { - if (availableFields.length > pageSize) { - return [availableFields.slice(0, pageSize), []]; - } else { - return [availableFields, emptyFields.slice(0, pageSize - availableFields.length)]; - } - } - if (isAvailableAccordionOpen && !isEmptyAccordionOpen) { - return [availableFields.slice(0, pageSize), []]; - } - - if (!isAvailableAccordionOpen && isEmptyAccordionOpen) { - return [[], emptyFields.slice(0, pageSize)]; - } - return [[], []]; - }, [ - localState.isAvailableAccordionOpen, - localState.isEmptyAccordionOpen, - filteredFieldGroups, - pageSize, - ]); + return Object.fromEntries( + Object.entries(unfilteredFieldGroups).map(([name, group]) => [ + name, + { ...group, fields: filterFieldGroup(group.fields) }, + ]) + ); + }, [unfilteredFieldGroups, localState.nameFilter, localState.typeFilter]); const fieldProps = useMemo( () => ({ @@ -423,8 +423,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ ] ); - const fieldInfoUnavailable = existenceFetchFailed || currentIndexPattern.hasRestrictions; - return ( } > - ( @@ -545,115 +543,21 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ -
    { - if (el && !el.dataset.dynamicScroll) { - el.dataset.dynamicScroll = 'true'; - setScrollContainer(el); - } + + field.type === 'document' || + fieldExists(existingFields, currentIndexPattern.title, field.name) + } + fieldProps={fieldProps} + fieldGroups={fieldGroups} + hasSyncedExistingFields={!!hasSyncedExistingFields} + filter={{ + nameFilter: localState.nameFilter, + typeFilter: localState.typeFilter, }} - onScroll={throttle(lazyScroll, 100)} - > -
    - {filteredFieldGroups.specialFields.map((field: IndexPatternField) => ( - - ))} - - - { - setLocalState((s) => ({ - ...s, - isAvailableAccordionOpen: open, - })); - const displayedFieldLength = - (open ? filteredFieldGroups.availableFields.length : 0) + - (localState.isEmptyAccordionOpen ? filteredFieldGroups.emptyFields.length : 0); - setPageSize( - Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength)) - ); - }} - showExistenceFetchError={existenceFetchFailed} - renderCallout={ - - } - /> - - {!fieldInfoUnavailable && ( - { - setLocalState((s) => ({ - ...s, - isEmptyAccordionOpen: open, - })); - const displayedFieldLength = - (localState.isAvailableAccordionOpen - ? filteredFieldGroups.availableFields.length - : 0) + (open ? filteredFieldGroups.emptyFields.length : 0); - setPageSize( - Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength)) - ); - }} - renderCallout={ - - } - /> - )} - -
    -
    + currentIndexPatternId={currentIndexPatternId} + existenceFetchFailed={existenceFetchFailed} + />
    diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index 60f60d7cb80c1..e71a85868b855 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -116,7 +116,8 @@ export function FieldSelect({ })); } - const [availableFields, emptyFields] = _.partition(normalFields, containsData); + const [metaFields, nonMetaFields] = _.partition(normalFields, (field) => fieldMap[field].meta); + const [availableFields, emptyFields] = _.partition(nonMetaFields, containsData); const constructFieldsOptions = (fieldsArr: string[], label: string) => fieldsArr.length > 0 && { @@ -138,10 +139,18 @@ export function FieldSelect({ }) ); + const metaFieldsOptions = constructFieldsOptions( + metaFields, + i18n.translate('xpack.lens.indexPattern.metaFieldsLabel', { + defaultMessage: 'Meta fields', + }) + ); + return [ ...fieldNamesToOptions(specialFields), availableFieldsOptions, emptyFieldsOptions, + metaFieldsOptions, ].filter(Boolean); }, [ incompatibleSelectedOperationType, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index 1f6d7911b3a33..1eeb64127310f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -184,7 +184,8 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { defaultMessage: 'Click for a field preview, or drag and drop to visualize.', }) : i18n.translate('xpack.lens.indexPattern.fieldStatsButtonEmptyLabel', { - defaultMessage: "This field doesn't have data. Drag and drop to visualize.", + defaultMessage: + 'This field doesn’t have any data but you can still drag and drop to visualize.', }) } type="iInCircle" @@ -307,7 +308,7 @@ function FieldItemPopoverContents(props: State & FieldItemProps) { {i18n.translate('xpack.lens.indexPattern.fieldStatsNoData', { defaultMessage: - 'This field is empty because it doesn’t exist in the 500 sampled documents.', + 'This field is empty because it doesn’t exist in the 500 sampled documents. Adding this field to the configuration may result in a blank chart.', })} ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.scss b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.scss new file mode 100644 index 0000000000000..f28581b835b07 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.scss @@ -0,0 +1,20 @@ +/** + * 1. Don't cut off the shadow of the field items + */ + +.lnsIndexPatternFieldList { + @include euiOverflowShadow; + @include euiScrollBar; + margin-left: -$euiSize; /* 1 */ + position: relative; + flex-grow: 1; + overflow: auto; +} + +.lnsIndexPatternFieldList__accordionContainer { + padding-top: $euiSizeS; + position: absolute; + top: 0; + left: $euiSize; /* 1 */ + right: $euiSizeXS; /* 1 */ +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx new file mode 100644 index 0000000000000..4a9b3a0c63e3f --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import './field_list.scss'; +import { throttle } from 'lodash'; +import React, { useState, Fragment, useCallback, useMemo, useEffect } from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { FieldItem } from './field_item'; +import { NoFieldsCallout } from './no_fields_callout'; +import { IndexPatternField } from './types'; +import { FieldItemSharedProps, FieldsAccordion } from './fields_accordion'; +const PAGINATION_SIZE = 50; + +export interface FieldsGroup { + specialFields: IndexPatternField[]; + availableFields: IndexPatternField[]; + emptyFields: IndexPatternField[]; + metaFields: IndexPatternField[]; +} + +export type FieldGroups = Record< + string, + { + fields: IndexPatternField[]; + fieldCount: number; + showInAccordion: boolean; + isInitiallyOpen: boolean; + title: string; + isAffectedByGlobalFilter: boolean; + isAffectedByTimeFilter: boolean; + hideDetails?: boolean; + } +>; + +function getDisplayedFieldsLength( + fieldGroups: FieldGroups, + accordionState: Partial> +) { + return Object.entries(fieldGroups) + .filter(([key]) => accordionState[key]) + .reduce((allFieldCount, [, { fields }]) => allFieldCount + fields.length, 0); +} + +export function FieldList({ + exists, + fieldGroups, + existenceFetchFailed, + fieldProps, + hasSyncedExistingFields, + filter, + currentIndexPatternId, +}: { + exists: (field: IndexPatternField) => boolean; + fieldGroups: FieldGroups; + fieldProps: FieldItemSharedProps; + hasSyncedExistingFields: boolean; + existenceFetchFailed?: boolean; + filter: { + nameFilter: string; + typeFilter: string[]; + }; + currentIndexPatternId: string; +}) { + const [pageSize, setPageSize] = useState(PAGINATION_SIZE); + const [scrollContainer, setScrollContainer] = useState(undefined); + const [accordionState, setAccordionState] = useState>>(() => + Object.fromEntries( + Object.entries(fieldGroups) + .filter(([, { showInAccordion }]) => showInAccordion) + .map(([key, { isInitiallyOpen }]) => [key, isInitiallyOpen]) + ) + ); + + const isAffectedByFieldFilter = !!(filter.typeFilter.length || filter.nameFilter.length); + + useEffect(() => { + // Reset the scroll if we have made material changes to the field list + if (scrollContainer) { + scrollContainer.scrollTop = 0; + setPageSize(PAGINATION_SIZE); + } + }, [filter.nameFilter, filter.typeFilter, currentIndexPatternId, scrollContainer]); + + const lazyScroll = useCallback(() => { + if (scrollContainer) { + const nearBottom = + scrollContainer.scrollTop + scrollContainer.clientHeight > + scrollContainer.scrollHeight * 0.9; + if (nearBottom) { + setPageSize( + Math.max( + PAGINATION_SIZE, + Math.min( + pageSize + PAGINATION_SIZE * 0.5, + getDisplayedFieldsLength(fieldGroups, accordionState) + ) + ) + ); + } + } + }, [scrollContainer, pageSize, setPageSize, fieldGroups, accordionState]); + + const paginatedFields = useMemo(() => { + let remainingItems = pageSize; + return Object.fromEntries( + Object.entries(fieldGroups) + .filter(([, { showInAccordion }]) => showInAccordion) + .map(([key, fieldGroup]) => { + if (!accordionState[key] || remainingItems <= 0) { + return [key, []]; + } + const slicedFieldList = fieldGroup.fields.slice(0, remainingItems); + remainingItems = remainingItems - slicedFieldList.length; + return [key, slicedFieldList]; + }) + ); + }, [pageSize, fieldGroups, accordionState]); + + return ( +
    { + if (el && !el.dataset.dynamicScroll) { + el.dataset.dynamicScroll = 'true'; + setScrollContainer(el); + } + }} + onScroll={throttle(lazyScroll, 100)} + > +
    + {Object.entries(fieldGroups) + .filter(([, { showInAccordion }]) => !showInAccordion) + .flatMap(([, { fields }]) => + fields.map((field) => ( + + )) + )} + + {Object.entries(fieldGroups) + .filter(([, { showInAccordion }]) => showInAccordion) + .map(([key, fieldGroup]) => ( + + { + setAccordionState((s) => ({ + ...s, + [key]: open, + })); + const displayedFieldLength = getDisplayedFieldsLength(fieldGroups, { + ...accordionState, + [key]: open, + }); + setPageSize( + Math.max(PAGINATION_SIZE, Math.min(pageSize * 1.5, displayedFieldLength)) + ); + }} + showExistenceFetchError={existenceFetchFailed} + renderCallout={ + + } + /> + + + ))} +
    +
    + ); +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx index b0604efff7b89..7d1c80e5a7f6a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.test.tsx @@ -71,11 +71,19 @@ describe('Fields Accordion', () => { paginatedFields: indexPattern.fields, fieldProps, renderCallout:
    Callout
    , - exists: true, + exists: () => true, }; }); it('renders correct number of Field Items', () => { + const wrapper = mountWithIntl( + field.name === 'timestamp'} /> + ); + expect(wrapper.find(FieldItem).at(0).prop('exists')).toEqual(true); + expect(wrapper.find(FieldItem).at(1).prop('exists')).toEqual(false); + }); + + it('passed correct exists flag to each field', () => { const wrapper = mountWithIntl(); expect(wrapper.find(FieldItem).length).toEqual(2); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx index 30a92c21ff661..e531eb72f94ca 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx @@ -45,7 +45,7 @@ export interface FieldsAccordionProps { paginatedFields: IndexPatternField[]; fieldProps: FieldItemSharedProps; renderCallout: JSX.Element; - exists: boolean; + exists: (field: IndexPatternField) => boolean; showExistenceFetchError?: boolean; hideDetails?: boolean; } @@ -71,7 +71,7 @@ export const InnerFieldsAccordion = function InnerFieldsAccordion({ {...fieldProps} key={field.name} field={field} - exists={exists} + exists={exists(field)} hideDetails={hideDetails} /> ), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index 19213d4afc9bc..ef6abbec9a34d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -197,7 +197,7 @@ function mockClient() { function mockIndexPatternsService() { return ({ get: jest.fn(async (id: '1' | '2') => { - return sampleIndexPatternsFromService[id]; + return { ...sampleIndexPatternsFromService[id], metaFields: [] }; }), } as unknown) as Pick; } @@ -248,6 +248,7 @@ describe('loader', () => { get: jest.fn(async () => ({ id: 'foo', title: 'Foo index', + metaFields: [], typeMeta: { aggs: { date_histogram: { @@ -295,6 +296,55 @@ describe('loader', () => { date_histogram: { agg: 'date_histogram', fixed_interval: 'm' }, }); }); + + it('should map meta flag', async () => { + const cache = await loadIndexPatterns({ + cache: {}, + patterns: ['foo'], + indexPatternsService: ({ + get: jest.fn(async () => ({ + id: 'foo', + title: 'Foo index', + metaFields: ['timestamp'], + typeMeta: { + aggs: { + date_histogram: { + timestamp: { + agg: 'date_histogram', + fixed_interval: 'm', + }, + }, + sum: { + bytes: { + agg: 'sum', + }, + }, + }, + }, + fields: [ + { + name: 'timestamp', + displayName: 'timestampLabel', + type: 'date', + aggregatable: true, + searchable: true, + }, + { + name: 'bytes', + displayName: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ], + })), + } as unknown) as Pick, + }); + + expect(cache.foo.fields.find((f: IndexPatternField) => f.name === 'timestamp')!.meta).toEqual( + true + ); + }); }); describe('loadInitialState', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 0ab658b961336..c4b1eb9e0c4c4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -63,6 +63,7 @@ export async function loadIndexPatterns({ type: field.type, aggregatable: field.aggregatable, searchable: field.searchable, + meta: indexPattern.metaFields.includes(field.name), esTypes: field.esTypes, scripted: field.scripted, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index b691c5b5c4c40..a3c0e8aed7421 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -26,6 +26,7 @@ export interface IndexPattern { export type IndexPatternField = IFieldType & { displayName: string; aggregationRestrictions?: Partial; + meta?: boolean; }; export interface IndexPatternLayer { diff --git a/x-pack/plugins/lens/server/routes/existing_fields.test.ts b/x-pack/plugins/lens/server/routes/existing_fields.test.ts index 728b78c8e97bc..9799dcf92ae41 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.test.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.test.ts @@ -15,6 +15,7 @@ describe('existingFields', () => { name, isScript: false, isAlias: false, + isMeta: false, path: name.split('.'), ...obj, }; @@ -101,6 +102,15 @@ describe('existingFields', () => { expect(result).toEqual(['baz']); }); + + it('supports meta fields', () => { + const result = existingFields( + [{ _mymeta: 'abc', ...indexPattern({}, { bar: 'scriptvalue' }) }], + [field({ name: '_mymeta', isMeta: true, path: ['_mymeta'] })] + ); + + expect(result).toEqual(['_mymeta']); + }); }); describe('buildFieldList', () => { @@ -116,6 +126,7 @@ describe('buildFieldList', () => { { name: 'bar' }, { name: '@bar' }, { name: 'baz' }, + { name: '_mymeta' }, ]), }, references: [], @@ -142,7 +153,7 @@ describe('buildFieldList', () => { ]; it('uses field descriptors to determine the path', () => { - const fields = buildFieldList(indexPattern, mappings, fieldDescriptors); + const fields = buildFieldList(indexPattern, mappings, fieldDescriptors, []); expect(fields.find((f) => f.name === 'baz')).toMatchObject({ isAlias: false, isScript: false, @@ -152,7 +163,7 @@ describe('buildFieldList', () => { }); it('uses aliases to determine the path', () => { - const fields = buildFieldList(indexPattern, mappings, fieldDescriptors); + const fields = buildFieldList(indexPattern, mappings, fieldDescriptors, []); expect(fields.find((f) => f.isAlias)).toMatchObject({ isAlias: true, isScript: false, @@ -162,7 +173,7 @@ describe('buildFieldList', () => { }); it('supports scripted fields', () => { - const fields = buildFieldList(indexPattern, mappings, fieldDescriptors); + const fields = buildFieldList(indexPattern, mappings, fieldDescriptors, []); expect(fields.find((f) => f.isScript)).toMatchObject({ isAlias: false, isScript: true, @@ -173,13 +184,24 @@ describe('buildFieldList', () => { }); }); + it('supports meta fields', () => { + const fields = buildFieldList(indexPattern, mappings, fieldDescriptors, ['_mymeta']); + expect(fields.find((f) => f.isMeta)).toMatchObject({ + isAlias: false, + isScript: false, + isMeta: true, + name: '_mymeta', + path: ['_mymeta'], + }); + }); + it('handles missing mappings', () => { - const fields = buildFieldList(indexPattern, {}, fieldDescriptors); + const fields = buildFieldList(indexPattern, {}, fieldDescriptors, []); expect(fields.every((f) => f.isAlias === false)).toEqual(true); }); it('handles empty fieldDescriptors by skipping multi-mappings', () => { - const fields = buildFieldList(indexPattern, mappings, []); + const fields = buildFieldList(indexPattern, mappings, [], []); expect(fields.find((f) => f.name === 'baz')).toMatchObject({ isAlias: false, isScript: false, diff --git a/x-pack/plugins/lens/server/routes/existing_fields.ts b/x-pack/plugins/lens/server/routes/existing_fields.ts index 7ab3cdceb2145..33fcafacfad73 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.ts @@ -12,6 +12,7 @@ import { BASE_API_URL } from '../../common'; import { IndexPatternsFetcher, IndexPatternAttributes, + UI_SETTINGS, } from '../../../../../src/plugins/data/server'; /** @@ -36,13 +37,12 @@ export interface Field { name: string; isScript: boolean; isAlias: boolean; + isMeta: boolean; path: string[]; lang?: string; script?: string; } -const metaFields = ['_source', '_type']; - export async function existingFieldsRoute(setup: CoreSetup) { const router = setup.http.createRouter(); @@ -104,14 +104,15 @@ async function fetchFieldExistence({ toDate?: string; timeFieldName?: string; }) { + const metaFields: string[] = await context.core.uiSettings.client.get(UI_SETTINGS.META_FIELDS); const { indexPattern, indexPatternTitle, mappings, fieldDescriptors, - } = await fetchIndexPatternDefinition(indexPatternId, context); + } = await fetchIndexPatternDefinition(indexPatternId, context, metaFields); - const fields = buildFieldList(indexPattern, mappings, fieldDescriptors); + const fields = buildFieldList(indexPattern, mappings, fieldDescriptors, metaFields); const docs = await fetchIndexPatternStats({ fromDate, toDate, @@ -128,7 +129,11 @@ async function fetchFieldExistence({ }; } -async function fetchIndexPatternDefinition(indexPatternId: string, context: RequestHandlerContext) { +async function fetchIndexPatternDefinition( + indexPatternId: string, + context: RequestHandlerContext, + metaFields: string[] +) { const savedObjectsClient = context.core.savedObjects.client; const requestClient = context.core.elasticsearch.legacy.client; const indexPattern = await savedObjectsClient.get( @@ -178,7 +183,8 @@ async function fetchIndexPatternDefinition(indexPatternId: string, context: Requ export function buildFieldList( indexPattern: SavedObject, mappings: MappingResult | {}, - fieldDescriptors: FieldDescriptor[] + fieldDescriptors: FieldDescriptor[], + metaFields: string[] ): Field[] { const aliasMap = Object.entries(Object.values(mappings)[0]?.mappings.properties ?? {}) .map(([name, v]) => ({ ...v, name })) @@ -204,6 +210,9 @@ export function buildFieldList( path: path.split('.'), lang: field.lang, script: field.script, + // id is a special case - it doesn't show up in the meta field list, + // but as it's not part of source, it has to be handled separately. + isMeta: metaFields.includes(field.name) || field.name === '_id', }; } ); @@ -312,7 +321,7 @@ function exists(obj: unknown, path: string[], i = 0): boolean { * Exported only for unit tests. */ export function existingFields( - docs: Array<{ _source: unknown; fields: unknown }>, + docs: Array<{ _source: unknown; fields: unknown; [key: string]: unknown }>, fields: Field[] ): string[] { const missingFields = new Set(fields); @@ -323,7 +332,14 @@ export function existingFields( } missingFields.forEach((field) => { - if (exists(field.isScript ? doc.fields : doc._source, field.path)) { + let fieldStore = doc._source; + if (field.isScript) { + fieldStore = doc.fields; + } + if (field.isMeta) { + fieldStore = doc; + } + if (exists(fieldStore, field.path)) { missingFields.delete(field); } }); diff --git a/x-pack/test/api_integration/apis/lens/existing_fields.ts b/x-pack/test/api_integration/apis/lens/existing_fields.ts index 92336f2892f43..10ee7bc9b48ea 100644 --- a/x-pack/test/api_integration/apis/lens/existing_fields.ts +++ b/x-pack/test/api_integration/apis/lens/existing_fields.ts @@ -20,6 +20,9 @@ const fieldsWithData = [ '@tags', '@tags.raw', '@timestamp', + '_id', + '_index', + '_source', 'agent', 'agent.raw', 'bytes', @@ -96,6 +99,9 @@ const fieldsWithData = [ const metricBeatData = [ '@timestamp', + '_id', + '_index', + '_source', 'agent.ephemeral_id', 'agent.hostname', 'agent.id', @@ -185,6 +191,9 @@ export default ({ getService }: FtrProviderContext) => { '@tags', '@tags.raw', '@timestamp', + '_id', + '_index', + '_source', 'agent', 'agent.raw', 'bytes', From 29bc00c04cf3725deca930399becab6f360993b2 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Thu, 24 Sep 2020 12:15:25 +0300 Subject: [PATCH 84/92] disable incremental build for x-pack tests (#78131) Co-authored-by: Elastic Machine --- x-pack/test/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index e8af79b9e84e0..3736d957a55a6 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "tsBuildInfoFile": "../../build/tsbuildinfo/x-pack/test", + // overhead is too significant + "incremental": false, "types": [ "mocha", "node", From 5d5ce401680412dc4aefcb2bd0cab16b3ce76fda Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 24 Sep 2020 11:42:35 +0200 Subject: [PATCH 85/92] fix drilldown in tsvb (#78005) --- .../application/components/vis_types/table/vis.js | 11 ++++++++--- .../application/components/vis_types/top_n/vis.js | 4 +++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js index d55afeda62e70..1341cf02202a0 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/table/vis.js @@ -20,6 +20,7 @@ import _, { isArray, last, get } from 'lodash'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { RedirectAppLinks } from '../../../../../../kibana_react/public'; import { createTickFormatter } from '../../lib/tick_formatter'; import { calculateLabel } from '../../../../../../../plugins/vis_type_timeseries/common/calculate_label'; import { isSortable } from './is_sortable'; @@ -27,7 +28,7 @@ import { EuiToolTip, EuiIcon } from '@elastic/eui'; import { replaceVars } from '../../lib/replace_vars'; import { fieldFormats } from '../../../../../../../plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getFieldFormats } from '../../../../services'; +import { getFieldFormats, getCoreStart } from '../../../../services'; import { METRIC_TYPES } from '../../../../../../../plugins/vis_type_timeseries/common/metric_types'; @@ -231,12 +232,16 @@ export class TableVis extends Component { ); } return ( -
    + {header}{rows}
    -
    + ); } } diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js index a4fe6f796bc0b..e9f64c93d337f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js @@ -17,6 +17,7 @@ * under the License. */ +import { getCoreStart } from '../../../../services'; import { createTickFormatter } from '../../lib/tick_formatter'; import { TopN } from '../../../visualizations/views/top_n'; import { getLastValue } from '../../../../../../../plugins/vis_type_timeseries/common/get_last_value'; @@ -89,7 +90,8 @@ export function TopNVisualization(props) { if (model.drilldown_url) { params.onClick = (item) => { - window.location = replaceVars(model.drilldown_url, {}, { key: item.label }); + const url = replaceVars(model.drilldown_url, {}, { key: item.label }); + getCoreStart().application.navigateToUrl(url); }; } From 88b03d943b7631161136dd0bf8201e3eff919c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 24 Sep 2020 10:49:31 +0100 Subject: [PATCH 86/92] [Usage Collection] [schema] `static_telemetry` (#77902) Co-authored-by: Elastic Machine --- .telemetryrc.json | 3 +- src/plugins/telemetry/schema/oss_plugins.json | 115 ++++++++++++++++++ .../server/collectors/usage/schema.ts | 58 +++++++++ .../usage/telemetry_usage_collector.ts | 49 +++++++- 4 files changed, 219 insertions(+), 6 deletions(-) create mode 100644 src/plugins/telemetry/server/collectors/usage/schema.ts diff --git a/.telemetryrc.json b/.telemetryrc.json index 818f9805628e1..13bb6e3ae88c0 100644 --- a/.telemetryrc.json +++ b/.telemetryrc.json @@ -8,8 +8,7 @@ "src/plugins/kibana_utils/", "src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts", "src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts", - "src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts", - "src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts" + "src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts" ] } ] diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 5bce03a292760..6662482402fc5 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -1310,6 +1310,121 @@ } } }, + "static_telemetry": { + "properties": { + "ece": { + "properties": { + "kb_uuid": { + "type": "keyword" + }, + "es_uuid": { + "type": "keyword" + }, + "account_id": { + "type": "keyword" + }, + "license": { + "properties": { + "uuid": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "issued_to": { + "type": "text" + }, + "issuer": { + "type": "text" + }, + "issue_date_in_millis": { + "type": "long" + }, + "start_date_in_millis": { + "type": "long" + }, + "expiry_date_in_millis": { + "type": "long" + }, + "max_resource_units": { + "type": "long" + } + } + } + } + }, + "ess": { + "properties": { + "kb_uuid": { + "type": "keyword" + }, + "es_uuid": { + "type": "keyword" + }, + "account_id": { + "type": "keyword" + }, + "license": { + "properties": { + "uuid": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "issued_to": { + "type": "text" + }, + "issuer": { + "type": "text" + }, + "issue_date_in_millis": { + "type": "long" + }, + "start_date_in_millis": { + "type": "long" + }, + "expiry_date_in_millis": { + "type": "long" + }, + "max_resource_units": { + "type": "long" + } + } + } + } + }, + "eck": { + "properties": { + "operator_uuid": { + "type": "keyword" + }, + "operator_roles": { + "type": "keyword" + }, + "custom_operator_namespace": { + "type": "boolean" + }, + "distribution": { + "type": "text" + }, + "build": { + "properties": { + "hash": { + "type": "text" + }, + "date": { + "type": "date" + }, + "version": { + "type": "keyword" + } + } + } + } + } + } + }, "tsvb-validation": { "properties": { "failed_validations": { diff --git a/src/plugins/telemetry/server/collectors/usage/schema.ts b/src/plugins/telemetry/server/collectors/usage/schema.ts new file mode 100644 index 0000000000000..8f4d555d75c49 --- /dev/null +++ b/src/plugins/telemetry/server/collectors/usage/schema.ts @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MakeSchemaFrom } from 'src/plugins/usage_collection/server'; +import { LicenseUsage, StaticTelemetryUsage } from './telemetry_usage_collector'; + +const licenseSchema: MakeSchemaFrom = { + uuid: { type: 'keyword' }, + type: { type: 'keyword' }, + issued_to: { type: 'text' }, + issuer: { type: 'text' }, + issue_date_in_millis: { type: 'long' }, + start_date_in_millis: { type: 'long' }, + expiry_date_in_millis: { type: 'long' }, + max_resource_units: { type: 'long' }, +}; + +export const staticTelemetrySchema: MakeSchemaFrom> = { + ece: { + kb_uuid: { type: 'keyword' }, + es_uuid: { type: 'keyword' }, + account_id: { type: 'keyword' }, + license: licenseSchema, + }, + ess: { + kb_uuid: { type: 'keyword' }, + es_uuid: { type: 'keyword' }, + account_id: { type: 'keyword' }, + license: licenseSchema, + }, + eck: { + operator_uuid: { type: 'keyword' }, + operator_roles: { type: 'keyword' }, + custom_operator_namespace: { type: 'boolean' }, + distribution: { type: 'text' }, + build: { + hash: { type: 'text' }, + date: { type: 'date' }, + version: { type: 'keyword' }, + }, + }, +}; diff --git a/src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts b/src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts index bde7cfa5c4445..39f8ef0151a0b 100644 --- a/src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts +++ b/src/plugins/telemetry/server/collectors/usage/telemetry_usage_collector.ts @@ -29,6 +29,7 @@ import { TelemetryConfigType } from '../../config'; // look for telemetry.yml in the same places we expect kibana.yml import { ensureDeepObject } from './ensure_deep_object'; +import { staticTelemetrySchema } from './schema'; /** * The maximum file size before we ignore it (note: this limit is arbitrary). @@ -60,10 +61,12 @@ export function isFileReadable(path: string): boolean { * @param configPath The config file path. * @returns The unmodified JSON object if the file exists and is a valid YAML file. */ -export async function readTelemetryFile(path: string): Promise { +export async function readTelemetryFile( + configPath: string +): Promise { try { - if (isFileReadable(path)) { - const yaml = readFileSync(path); + if (isFileReadable(configPath)) { + const yaml = readFileSync(configPath); const data = safeLoad(yaml.toString()); // don't bother returning empty objects @@ -79,11 +82,48 @@ export async function readTelemetryFile(path: string): Promise Promise ) { - return usageCollection.makeUsageCollector({ + return usageCollection.makeUsageCollector({ type: 'static_telemetry', isReady: () => true, fetch: async () => { @@ -91,6 +131,7 @@ export function createTelemetryUsageCollector( const telemetryPath = join(dirname(configPath), 'telemetry.yml'); return await readTelemetryFile(telemetryPath); }, + schema: staticTelemetrySchema, }); } From 9ca22382fb9f4aca147e07ac9a42bdb1e9d737e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 24 Sep 2020 10:51:42 +0100 Subject: [PATCH 87/92] [Usage Collection] [Schema] "kibana" collector (#77893) Co-authored-by: Elastic Machine --- .telemetryrc.json | 1 - .../kibana/get_saved_object_counts.ts | 11 +++-- .../kibana/kibana_usage_collector.ts | 17 ++++++- src/plugins/telemetry/schema/oss_plugins.json | 49 +++++++++++++++++++ 4 files changed, 71 insertions(+), 7 deletions(-) diff --git a/.telemetryrc.json b/.telemetryrc.json index 13bb6e3ae88c0..7d9743b20ff68 100644 --- a/.telemetryrc.json +++ b/.telemetryrc.json @@ -6,7 +6,6 @@ "src/plugins/kibana_react/", "src/plugins/testbed/", "src/plugins/kibana_utils/", - "src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts", "src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts", "src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts" ] diff --git a/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.ts b/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.ts index 1adc0dc6896fd..e88d90fe5b24b 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/kibana/get_saved_object_counts.ts @@ -39,9 +39,12 @@ const TYPES = [ ]; export interface KibanaSavedObjectCounts { - [pluginName: string]: { - total: number; - }; + dashboard: { total: number }; + visualization: { total: number }; + search: { total: number }; + index_pattern: { total: number }; + graph_workspace: { total: number }; + timelion_sheet: { total: number }; } export async function getSavedObjectsCounts( @@ -71,7 +74,7 @@ export async function getSavedObjectsCounts( // Initialise the object with all zeros for all the types const allZeros: KibanaSavedObjectCounts = TYPES.reduce( (acc, type) => ({ ...acc, [snakeCase(type)]: { total: 0 } }), - {} + {} as KibanaSavedObjectCounts ); // Add the doc_count from each bucket diff --git a/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts index 9cc079a9325d5..5b56e1a9b596f 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/kibana/kibana_usage_collector.ts @@ -22,15 +22,28 @@ import { take } from 'rxjs/operators'; import { SharedGlobalConfig } from 'kibana/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { KIBANA_STATS_TYPE } from '../../../common/constants'; -import { getSavedObjectsCounts } from './get_saved_object_counts'; +import { getSavedObjectsCounts, KibanaSavedObjectCounts } from './get_saved_object_counts'; + +interface KibanaUsage extends KibanaSavedObjectCounts { + index: string; +} export function getKibanaUsageCollector( usageCollection: UsageCollectionSetup, legacyConfig$: Observable ) { - return usageCollection.makeUsageCollector({ + return usageCollection.makeUsageCollector({ type: 'kibana', isReady: () => true, + schema: { + index: { type: 'keyword' }, + dashboard: { total: { type: 'long' } }, + visualization: { total: { type: 'long' } }, + search: { total: { type: 'long' } }, + index_pattern: { total: { type: 'long' } }, + graph_workspace: { total: { type: 'long' } }, + timelion_sheet: { total: { type: 'long' } }, + }, async fetch(callCluster) { const { kibana: { index }, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 6662482402fc5..a83cd5a562ff6 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -1297,6 +1297,55 @@ } } }, + "kibana": { + "properties": { + "index": { + "type": "keyword" + }, + "dashboard": { + "properties": { + "total": { + "type": "long" + } + } + }, + "visualization": { + "properties": { + "total": { + "type": "long" + } + } + }, + "search": { + "properties": { + "total": { + "type": "long" + } + } + }, + "index_pattern": { + "properties": { + "total": { + "type": "long" + } + } + }, + "graph_workspace": { + "properties": { + "total": { + "type": "long" + } + } + }, + "timelion_sheet": { + "properties": { + "total": { + "type": "long" + } + } + } + } + }, "telemetry": { "properties": { "opt_in_status": { From 8ad53d52037bc9c5842e5a74766ec6fc08fd5c94 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 24 Sep 2020 12:29:29 +0200 Subject: [PATCH 88/92] [Discover] Context - Fix bug when document id contains a slash (#77435) --- .../public/application/angular/context.js | 30 +++++++------------ src/plugins/discover/public/plugin.ts | 8 +++++ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/plugins/discover/public/application/angular/context.js b/src/plugins/discover/public/application/angular/context.js index 6223090aa9f97..bb9d71c8671a2 100644 --- a/src/plugins/discover/public/application/angular/context.js +++ b/src/plugins/discover/public/application/angular/context.js @@ -45,26 +45,18 @@ const k7Breadcrumbs = ($route) => { }; getAngularModule().config(($routeProvider) => { - $routeProvider - // deprecated route, kept for compatibility - // should be removed in the future - .when('/context/:indexPatternId/:type/:id*', { - redirectTo: '/context/:indexPatternId/:id', - }) - .when('/context/:indexPatternId/:id*', { - controller: ContextAppRouteController, - k7Breadcrumbs, - controllerAs: 'contextAppRoute', - resolve: { - indexPattern: ($route, Promise) => { - const indexPattern = getServices().indexPatterns.get( - $route.current.params.indexPatternId - ); - return Promise.props({ ip: indexPattern }); - }, + $routeProvider.when('/context/:indexPatternId/:id*', { + controller: ContextAppRouteController, + k7Breadcrumbs, + controllerAs: 'contextAppRoute', + resolve: { + indexPattern: ($route, Promise) => { + const indexPattern = getServices().indexPatterns.get($route.current.params.indexPatternId); + return Promise.props({ ip: indexPattern }); }, - template: contextAppRouteTemplate, - }); + }, + template: contextAppRouteTemplate, + }); }); function ContextAppRouteController($routeParams, $scope, $route) { diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index 440bd3fdf86d3..b1bbc89b62d9d 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -277,6 +277,14 @@ export class DiscoverPlugin return `#${path}`; }); plugins.urlForwarding.forwardApp('context', 'discover', (path) => { + const urlParts = path.split('/'); + // take care of urls containing legacy url, those split in the following way + // ["", "context", indexPatternId, _type, id + params] + if (urlParts[4]) { + // remove _type part + const newPath = [...urlParts.slice(0, 3), ...urlParts.slice(4)].join('/'); + return `#${newPath}`; + } return `#${path}`; }); plugins.urlForwarding.forwardApp('discover', 'discover', (path) => { From 4d08763af7ec6a1381ab8a9c2c29866d2e7a7923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 24 Sep 2020 11:40:59 +0100 Subject: [PATCH 89/92] [Usage Collection] [schema] `lens` (#77929) Co-authored-by: Elastic Machine --- .../__fixture__/parsed_working_collector.ts | 8 +- .../extract_collectors.test.ts.snap | 26 +- .../src/tools/serializer.test.ts | 26 +- .../src/tools/serializer.ts | 27 +- .../kbn-telemetry-tools/src/tools/utils.ts | 2 +- .../telemetry_collectors/constants.ts | 4 + x-pack/.telemetryrc.json | 1 - .../plugins/lens/server/usage/collectors.ts | 6 +- x-pack/plugins/lens/server/usage/schema.ts | 83 ++++ .../schema/xpack_plugins.json | 374 ++++++++++++++++++ 10 files changed, 528 insertions(+), 29 deletions(-) create mode 100644 x-pack/plugins/lens/server/usage/schema.ts diff --git a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts index b238c5aa346ad..54983278726eb 100644 --- a/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts +++ b/packages/kbn-telemetry-tools/src/tools/__fixture__/parsed_working_collector.ts @@ -75,11 +75,9 @@ export const parsedWorkingCollector: ParsedUsageCollection = [ type: 'StringKeyword', }, my_index_signature_prop: { - '': { - '@@INDEX@@': { - kind: SyntaxKind.NumberKeyword, - type: 'NumberKeyword', - }, + '@@INDEX@@': { + kind: SyntaxKind.NumberKeyword, + type: 'NumberKeyword', }, }, my_objects: { diff --git a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap index 68b068b0cfe06..9868a7d31d498 100644 --- a/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap +++ b/packages/kbn-telemetry-tools/src/tools/__snapshots__/extract_collectors.test.ts.snap @@ -96,16 +96,14 @@ Array [ "collectorName": "indexed_interface_with_not_matching_schema", "fetch": Object { "typeDescriptor": Object { - "": Object { - "@@INDEX@@": Object { - "count_1": Object { - "kind": 143, - "type": "NumberKeyword", - }, - "count_2": Object { - "kind": 143, - "type": "NumberKeyword", - }, + "@@INDEX@@": Object { + "count_1": Object { + "kind": 143, + "type": "NumberKeyword", + }, + "count_2": Object { + "kind": 143, + "type": "NumberKeyword", }, }, }, @@ -165,11 +163,9 @@ Array [ }, }, "my_index_signature_prop": Object { - "": Object { - "@@INDEX@@": Object { - "kind": 143, - "type": "NumberKeyword", - }, + "@@INDEX@@": Object { + "kind": 143, + "type": "NumberKeyword", }, }, "my_objects": Object { diff --git a/packages/kbn-telemetry-tools/src/tools/serializer.test.ts b/packages/kbn-telemetry-tools/src/tools/serializer.test.ts index 9475574a44219..6742117226368 100644 --- a/packages/kbn-telemetry-tools/src/tools/serializer.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/serializer.test.ts @@ -44,13 +44,13 @@ export function loadFixtureProgram(fixtureName: string) { } describe('getDescriptor', () => { - const usageInterfaces = new Map(); + const usageInterfaces = new Map(); let tsProgram: ts.Program; beforeAll(() => { const { program, sourceFile } = loadFixtureProgram('constants'); tsProgram = program; for (const node of traverseNodes(sourceFile)) { - if (ts.isInterfaceDeclaration(node)) { + if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) { const interfaceName = node.name.getText(); usageInterfaces.set(interfaceName, node); } @@ -102,4 +102,26 @@ describe('getDescriptor', () => { 'Mapping does not support conflicting union types.' ); }); + + it('serializes TypeAliasDeclaration', () => { + const usageInterface = usageInterfaces.get('TypeAliasWithUnion')!; + const descriptor = getDescriptor(usageInterface, tsProgram); + expect(descriptor).toEqual({ + locale: { kind: ts.SyntaxKind.StringKeyword, type: 'StringKeyword' }, + prop1: { kind: ts.SyntaxKind.StringKeyword, type: 'StringKeyword' }, + prop2: { kind: ts.SyntaxKind.StringKeyword, type: 'StringKeyword' }, + prop3: { kind: ts.SyntaxKind.StringKeyword, type: 'StringKeyword' }, + prop4: { kind: ts.SyntaxKind.StringLiteral, type: 'StringLiteral' }, + prop5: { kind: ts.SyntaxKind.FirstLiteralToken, type: 'FirstLiteralToken' }, + }); + }); + + it('serializes Record entries', () => { + const usageInterface = usageInterfaces.get('TypeAliasWithRecord')!; + const descriptor = getDescriptor(usageInterface, tsProgram); + expect(descriptor).toEqual({ + locale: { kind: ts.SyntaxKind.StringKeyword, type: 'StringKeyword' }, + '@@INDEX@@': { kind: ts.SyntaxKind.NumberKeyword, type: 'NumberKeyword' }, + }); + }); }); diff --git a/packages/kbn-telemetry-tools/src/tools/serializer.ts b/packages/kbn-telemetry-tools/src/tools/serializer.ts index 7afe828298b4b..6fe02e3824ba7 100644 --- a/packages/kbn-telemetry-tools/src/tools/serializer.ts +++ b/packages/kbn-telemetry-tools/src/tools/serializer.ts @@ -79,9 +79,13 @@ export function getDescriptor(node: ts.Node, program: ts.Program): Descriptor | } if (ts.isTypeLiteralNode(node) || ts.isInterfaceDeclaration(node)) { return node.members.reduce((acc, m) => { - acc[m.name?.getText() || ''] = getDescriptor(m, program); - return acc; - }, {} as any); + const key = m.name?.getText(); + if (key) { + return { ...acc, [key]: getDescriptor(m, program) }; + } else { + return { ...acc, ...getDescriptor(m, program) }; + } + }, {}); } // If it's defined as signature { [key: string]: OtherInterface } @@ -114,6 +118,10 @@ export function getDescriptor(node: ts.Node, program: ts.Program): Descriptor | if (symbolName === 'Date') { return { kind: TelemetryKinds.Date, type: 'Date' }; } + // Support `Record` + if (symbolName === 'Record' && node.typeArguments![0].kind === ts.SyntaxKind.StringKeyword) { + return { '@@INDEX@@': getDescriptor(node.typeArguments![1], program) }; + } const declaration = (symbol?.getDeclarations() || [])[0]; if (declaration) { return getDescriptor(declaration, program); @@ -157,6 +165,19 @@ export function getDescriptor(node: ts.Node, program: ts.Program): Descriptor | return uniqueKinds[0]; } + // Support `type MyUsageType = SomethingElse` + if (ts.isTypeAliasDeclaration(node)) { + return getDescriptor(node.type, program); + } + + // Support `&` unions + if (ts.isIntersectionTypeNode(node)) { + return node.types.reduce( + (acc, unionNode) => ({ ...acc, ...getDescriptor(unionNode, program) }), + {} + ); + } + switch (node.kind) { case ts.SyntaxKind.NumberKeyword: case ts.SyntaxKind.BooleanKeyword: diff --git a/packages/kbn-telemetry-tools/src/tools/utils.ts b/packages/kbn-telemetry-tools/src/tools/utils.ts index 3d6764117374c..e8e1b3fed1aef 100644 --- a/packages/kbn-telemetry-tools/src/tools/utils.ts +++ b/packages/kbn-telemetry-tools/src/tools/utils.ts @@ -249,7 +249,7 @@ export function difference(actual: any, expected: any) { function (result, value, key) { if (key && /@@INDEX@@/.test(`${key}`)) { // The type definition is an Index Signature, fuzzy searching for similar keys - const regexp = new RegExp(`${key}`.replace(/@@INDEX@@/g, '(.+)?')); + const regexp = new RegExp(`^${key}`.replace(/@@INDEX@@/g, '(.+)?')); const keysInBase = Object.keys(base) .map((k) => { const match = k.match(regexp); diff --git a/src/fixtures/telemetry_collectors/constants.ts b/src/fixtures/telemetry_collectors/constants.ts index 4aac9e66cdbdb..d4c9a1f85c4d7 100644 --- a/src/fixtures/telemetry_collectors/constants.ts +++ b/src/fixtures/telemetry_collectors/constants.ts @@ -51,3 +51,7 @@ export const externallyDefinedSchema: MakeSchemaFrom<{ locale: string }> = { type: 'keyword', }, }; + +export type TypeAliasWithUnion = Usage & WithUnion; + +export type TypeAliasWithRecord = Usage & Record; diff --git a/x-pack/.telemetryrc.json b/x-pack/.telemetryrc.json index 2c16491c1096b..30b2178259d68 100644 --- a/x-pack/.telemetryrc.json +++ b/x-pack/.telemetryrc.json @@ -7,7 +7,6 @@ "plugins/apm/server/lib/apm_telemetry/index.ts", "plugins/canvas/server/collectors/collector.ts", "plugins/infra/server/usage/usage_collector.ts", - "plugins/lens/server/usage/collectors.ts", "plugins/reporting/server/usage/reporting_usage_collector.ts", "plugins/maps/server/maps_telemetry/collectors/register.ts" ] diff --git a/x-pack/plugins/lens/server/usage/collectors.ts b/x-pack/plugins/lens/server/usage/collectors.ts index 3f033bd3b03d0..c32fc0371ed8a 100644 --- a/x-pack/plugins/lens/server/usage/collectors.ts +++ b/x-pack/plugins/lens/server/usage/collectors.ts @@ -10,6 +10,7 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { TaskManagerStartContract } from '../../../task_manager/server'; import { LensUsage, LensTelemetryState } from './types'; +import { lensUsageSchema } from './schema'; export function registerLensUsageCollector( usageCollection: UsageCollectionSetup, @@ -20,9 +21,9 @@ export function registerLensUsageCollector( // mark lensUsageCollector as ready to collect when the TaskManager is ready isCollectorReady = true; }); - const lensUsageCollector = usageCollection.makeUsageCollector({ + const lensUsageCollector = usageCollection.makeUsageCollector({ type: 'lens', - fetch: async (): Promise => { + async fetch() { try { const docs = await getLatestTaskState(await taskManager); // get the accumulated state from the recurring task @@ -55,6 +56,7 @@ export function registerLensUsageCollector( } }, isReady: () => isCollectorReady, + schema: lensUsageSchema, }); usageCollection.registerCollector(lensUsageCollector); diff --git a/x-pack/plugins/lens/server/usage/schema.ts b/x-pack/plugins/lens/server/usage/schema.ts new file mode 100644 index 0000000000000..a35d4d91845ee --- /dev/null +++ b/x-pack/plugins/lens/server/usage/schema.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MakeSchemaFrom } from 'src/plugins/usage_collection/server'; +import { LensUsage } from './types'; + +const eventsSchema: MakeSchemaFrom = { + app_query_change: { type: 'long' }, + indexpattern_field_info_click: { type: 'long' }, + loaded: { type: 'long' }, + app_filters_updated: { type: 'long' }, + app_date_change: { type: 'long' }, + save_failed: { type: 'long' }, + loaded_404: { type: 'long' }, + drop_total: { type: 'long' }, + chart_switch: { type: 'long' }, + suggestion_confirmed: { type: 'long' }, + suggestion_clicked: { type: 'long' }, + drop_onto_workspace: { type: 'long' }, + drop_non_empty: { type: 'long' }, + drop_empty: { type: 'long' }, + indexpattern_changed: { type: 'long' }, + indexpattern_filters_cleared: { type: 'long' }, + indexpattern_type_filter_toggled: { type: 'long' }, + indexpattern_existence_toggled: { type: 'long' }, + indexpattern_show_all_fields_clicked: { type: 'long' }, + drop_onto_dimension: { type: 'long' }, + indexpattern_dimension_removed: { type: 'long' }, + indexpattern_dimension_field_changed: { type: 'long' }, + xy_change_layer_display: { type: 'long' }, + xy_layer_removed: { type: 'long' }, + xy_layer_added: { type: 'long' }, + indexpattern_dimension_operation_terms: { type: 'long' }, + indexpattern_dimension_operation_date_histogram: { type: 'long' }, + indexpattern_dimension_operation_avg: { type: 'long' }, + indexpattern_dimension_operation_min: { type: 'long' }, + indexpattern_dimension_operation_max: { type: 'long' }, + indexpattern_dimension_operation_sum: { type: 'long' }, + indexpattern_dimension_operation_count: { type: 'long' }, + indexpattern_dimension_operation_cardinality: { type: 'long' }, + indexpattern_dimension_operation_filters: { type: 'long' }, +}; + +const suggestionEventsSchema: MakeSchemaFrom = { + back_to_current: { type: 'long' }, + reload: { type: 'long' }, +}; + +const savedSchema: MakeSchemaFrom = { + bar: { type: 'long' }, + bar_horizontal: { type: 'long' }, + line: { type: 'long' }, + area: { type: 'long' }, + bar_stacked: { type: 'long' }, + bar_percentage_stacked: { type: 'long' }, + bar_horizontal_stacked: { type: 'long' }, + bar_horizontal_percentage_stacked: { type: 'long' }, + area_stacked: { type: 'long' }, + area_percentage_stacked: { type: 'long' }, + lnsDatatable: { type: 'long' }, + lnsPie: { type: 'long' }, + lnsMetric: { type: 'long' }, +}; + +export const lensUsageSchema: MakeSchemaFrom = { + // LensClickUsage + events_30_days: eventsSchema, + events_90_days: eventsSchema, + suggestion_events_30_days: suggestionEventsSchema, + suggestion_events_90_days: suggestionEventsSchema, + + // LensVisualizationUsage + saved_overall_total: { type: 'long' }, + saved_30_days_total: { type: 'long' }, + saved_90_days_total: { type: 'long' }, + + saved_overall: savedSchema, + saved_30_days: savedSchema, + saved_90_days: savedSchema, +}; diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 904b14a7459ad..86b7889957c9f 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -155,6 +155,380 @@ } } }, + "lens": { + "properties": { + "events_30_days": { + "properties": { + "app_query_change": { + "type": "long" + }, + "indexpattern_field_info_click": { + "type": "long" + }, + "loaded": { + "type": "long" + }, + "app_filters_updated": { + "type": "long" + }, + "app_date_change": { + "type": "long" + }, + "save_failed": { + "type": "long" + }, + "loaded_404": { + "type": "long" + }, + "drop_total": { + "type": "long" + }, + "chart_switch": { + "type": "long" + }, + "suggestion_confirmed": { + "type": "long" + }, + "suggestion_clicked": { + "type": "long" + }, + "drop_onto_workspace": { + "type": "long" + }, + "drop_non_empty": { + "type": "long" + }, + "drop_empty": { + "type": "long" + }, + "indexpattern_changed": { + "type": "long" + }, + "indexpattern_filters_cleared": { + "type": "long" + }, + "indexpattern_type_filter_toggled": { + "type": "long" + }, + "indexpattern_existence_toggled": { + "type": "long" + }, + "indexpattern_show_all_fields_clicked": { + "type": "long" + }, + "drop_onto_dimension": { + "type": "long" + }, + "indexpattern_dimension_removed": { + "type": "long" + }, + "indexpattern_dimension_field_changed": { + "type": "long" + }, + "xy_change_layer_display": { + "type": "long" + }, + "xy_layer_removed": { + "type": "long" + }, + "xy_layer_added": { + "type": "long" + }, + "indexpattern_dimension_operation_terms": { + "type": "long" + }, + "indexpattern_dimension_operation_date_histogram": { + "type": "long" + }, + "indexpattern_dimension_operation_avg": { + "type": "long" + }, + "indexpattern_dimension_operation_min": { + "type": "long" + }, + "indexpattern_dimension_operation_max": { + "type": "long" + }, + "indexpattern_dimension_operation_sum": { + "type": "long" + }, + "indexpattern_dimension_operation_count": { + "type": "long" + }, + "indexpattern_dimension_operation_cardinality": { + "type": "long" + }, + "indexpattern_dimension_operation_filters": { + "type": "long" + } + } + }, + "events_90_days": { + "properties": { + "app_query_change": { + "type": "long" + }, + "indexpattern_field_info_click": { + "type": "long" + }, + "loaded": { + "type": "long" + }, + "app_filters_updated": { + "type": "long" + }, + "app_date_change": { + "type": "long" + }, + "save_failed": { + "type": "long" + }, + "loaded_404": { + "type": "long" + }, + "drop_total": { + "type": "long" + }, + "chart_switch": { + "type": "long" + }, + "suggestion_confirmed": { + "type": "long" + }, + "suggestion_clicked": { + "type": "long" + }, + "drop_onto_workspace": { + "type": "long" + }, + "drop_non_empty": { + "type": "long" + }, + "drop_empty": { + "type": "long" + }, + "indexpattern_changed": { + "type": "long" + }, + "indexpattern_filters_cleared": { + "type": "long" + }, + "indexpattern_type_filter_toggled": { + "type": "long" + }, + "indexpattern_existence_toggled": { + "type": "long" + }, + "indexpattern_show_all_fields_clicked": { + "type": "long" + }, + "drop_onto_dimension": { + "type": "long" + }, + "indexpattern_dimension_removed": { + "type": "long" + }, + "indexpattern_dimension_field_changed": { + "type": "long" + }, + "xy_change_layer_display": { + "type": "long" + }, + "xy_layer_removed": { + "type": "long" + }, + "xy_layer_added": { + "type": "long" + }, + "indexpattern_dimension_operation_terms": { + "type": "long" + }, + "indexpattern_dimension_operation_date_histogram": { + "type": "long" + }, + "indexpattern_dimension_operation_avg": { + "type": "long" + }, + "indexpattern_dimension_operation_min": { + "type": "long" + }, + "indexpattern_dimension_operation_max": { + "type": "long" + }, + "indexpattern_dimension_operation_sum": { + "type": "long" + }, + "indexpattern_dimension_operation_count": { + "type": "long" + }, + "indexpattern_dimension_operation_cardinality": { + "type": "long" + }, + "indexpattern_dimension_operation_filters": { + "type": "long" + } + } + }, + "suggestion_events_30_days": { + "properties": { + "back_to_current": { + "type": "long" + }, + "reload": { + "type": "long" + } + } + }, + "suggestion_events_90_days": { + "properties": { + "back_to_current": { + "type": "long" + }, + "reload": { + "type": "long" + } + } + }, + "saved_overall_total": { + "type": "long" + }, + "saved_30_days_total": { + "type": "long" + }, + "saved_90_days_total": { + "type": "long" + }, + "saved_overall": { + "properties": { + "bar": { + "type": "long" + }, + "bar_horizontal": { + "type": "long" + }, + "line": { + "type": "long" + }, + "area": { + "type": "long" + }, + "bar_stacked": { + "type": "long" + }, + "bar_percentage_stacked": { + "type": "long" + }, + "bar_horizontal_stacked": { + "type": "long" + }, + "bar_horizontal_percentage_stacked": { + "type": "long" + }, + "area_stacked": { + "type": "long" + }, + "area_percentage_stacked": { + "type": "long" + }, + "lnsDatatable": { + "type": "long" + }, + "lnsPie": { + "type": "long" + }, + "lnsMetric": { + "type": "long" + } + } + }, + "saved_30_days": { + "properties": { + "bar": { + "type": "long" + }, + "bar_horizontal": { + "type": "long" + }, + "line": { + "type": "long" + }, + "area": { + "type": "long" + }, + "bar_stacked": { + "type": "long" + }, + "bar_percentage_stacked": { + "type": "long" + }, + "bar_horizontal_stacked": { + "type": "long" + }, + "bar_horizontal_percentage_stacked": { + "type": "long" + }, + "area_stacked": { + "type": "long" + }, + "area_percentage_stacked": { + "type": "long" + }, + "lnsDatatable": { + "type": "long" + }, + "lnsPie": { + "type": "long" + }, + "lnsMetric": { + "type": "long" + } + } + }, + "saved_90_days": { + "properties": { + "bar": { + "type": "long" + }, + "bar_horizontal": { + "type": "long" + }, + "line": { + "type": "long" + }, + "area": { + "type": "long" + }, + "bar_stacked": { + "type": "long" + }, + "bar_percentage_stacked": { + "type": "long" + }, + "bar_horizontal_stacked": { + "type": "long" + }, + "bar_horizontal_percentage_stacked": { + "type": "long" + }, + "area_stacked": { + "type": "long" + }, + "area_percentage_stacked": { + "type": "long" + }, + "lnsDatatable": { + "type": "long" + }, + "lnsPie": { + "type": "long" + }, + "lnsMetric": { + "type": "long" + } + } + } + } + }, "mlTelemetry": { "properties": { "file_data_visualizer": { From 3618cef1a4a921ae73dfcee2785585beda2220c7 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Thu, 24 Sep 2020 13:26:00 +0200 Subject: [PATCH 90/92] [UX] Update csm app name to UX (#78179) --- .../support/step_definitions/csm/csm_dashboard.ts | 2 +- x-pack/plugins/apm/public/application/csmApp.tsx | 6 +++--- .../apm/public/components/app/RumDashboard/RumHome.tsx | 10 +++++----- .../ClientSideMonitoringCallout.tsx | 4 ++-- x-pack/plugins/apm/public/plugin.ts | 4 ++-- x-pack/plugins/apm/server/feature.ts | 8 ++++---- .../apps/apm/feature_controls/apm_security.ts | 4 ++-- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts index 461e2960c5e02..28af4fd5d8a56 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts @@ -16,7 +16,7 @@ Given(`a user browses the APM UI application for RUM Data`, () => { const RANGE_FROM = 'now-24h'; const RANGE_TO = 'now'; loginAndWaitForPage( - `/app/csm`, + `/app/ux`, { from: RANGE_FROM, to: RANGE_TO, diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index c63ec3700c877..5ebe14b663f56 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -20,7 +20,7 @@ import { import { APMRouteDefinition } from '../application/routes'; import { renderAsRedirectTo } from '../components/app/Main/route_config'; import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; -import { RumHome } from '../components/app/RumDashboard/RumHome'; +import { RumHome, UX_LABEL } from '../components/app/RumDashboard/RumHome'; import { ApmPluginContext } from '../context/ApmPluginContext'; import { LoadingIndicatorProvider } from '../context/LoadingIndicatorContext'; import { UrlParamsProvider } from '../context/UrlParamsContext'; @@ -39,8 +39,8 @@ export const rumRoutes: APMRouteDefinition[] = [ { exact: true, path: '/', - render: renderAsRedirectTo('/csm'), - breadcrumb: 'Client Side Monitoring', + render: renderAsRedirectTo('/ux'), + breadcrumb: UX_LABEL, }, ]; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx index 24da5e9ef3897..9abf792d7a0cf 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx @@ -10,6 +10,10 @@ import { i18n } from '@kbn/i18n'; import { RumOverview } from '../RumDashboard'; import { RumHeader } from './RumHeader'; +export const UX_LABEL = i18n.translate('xpack.apm.ux.title', { + defaultMessage: 'User Experience', +}); + export function RumHome() { return (
    @@ -17,11 +21,7 @@ export function RumHome() { -

    - {i18n.translate('xpack.apm.csm.title', { - defaultMessage: 'Client Side Monitoring', - })} -

    +

    {UX_LABEL}

    diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/ClientSideMonitoringCallout.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/ClientSideMonitoringCallout.tsx index b6938b211994d..becae4d7eb5d7 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/ClientSideMonitoringCallout.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/ClientSideMonitoringCallout.tsx @@ -11,14 +11,14 @@ import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; export function ClientSideMonitoringCallout() { const { core } = useApmPluginContext(); - const clientSideMonitoringHref = core.http.basePath.prepend(`/app/csm`); + const clientSideMonitoringHref = core.http.basePath.prepend(`/app/ux`); return ( diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index ab3f1026a92dd..dd9659a4cd1be 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -120,8 +120,8 @@ export class ApmPlugin implements Plugin { }); core.application.register({ - id: 'csm', - title: 'Client Side Monitoring', + id: 'ux', + title: 'User Experience', order: 8500, euiIconType: 'logoObservability', category: DEFAULT_APP_CATEGORIES.observability, diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index 14d8e2c3a4d50..75d8842d4843b 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -16,13 +16,13 @@ import { export const APM_FEATURE = { id: 'apm', name: i18n.translate('xpack.apm.featureRegistry.apmFeatureName', { - defaultMessage: 'APM and Client Side Monitoring', + defaultMessage: 'APM and User Experience', }), order: 900, category: DEFAULT_APP_CATEGORIES.observability, icon: 'apmApp', navLinkId: 'apm', - app: ['apm', 'csm', 'kibana'], + app: ['apm', 'ux', 'kibana'], catalogue: ['apm'], management: { insightsAndAlerting: ['triggersActions'], @@ -31,7 +31,7 @@ export const APM_FEATURE = { // see x-pack/plugins/features/common/feature_kibana_privileges.ts privileges: { all: { - app: ['apm', 'csm', 'kibana'], + app: ['apm', 'ux', 'kibana'], api: ['apm', 'apm_write'], catalogue: ['apm'], savedObject: { @@ -47,7 +47,7 @@ export const APM_FEATURE = { ui: ['show', 'save', 'alerting:show', 'alerting:save'], }, read: { - app: ['apm', 'csm', 'kibana'], + app: ['apm', 'ux', 'kibana'], api: ['apm'], catalogue: ['apm'], savedObject: { diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts index b93039c8fb0e4..3099057f65b80 100644 --- a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts @@ -63,7 +63,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(navLinks.map((link) => link.text)).to.eql([ 'Overview', 'APM', - 'Client Side Monitoring', + 'User Experience', 'Stack Management', ]); }); @@ -114,7 +114,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows apm navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'APM', 'Client Side Monitoring', 'Stack Management']); + expect(navLinks).to.eql(['Overview', 'APM', 'User Experience', 'Stack Management']); }); it('can navigate to APM app', async () => { From 89e1f087a23f82de2b5fb85dabc32cde2555885d Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 24 Sep 2020 15:02:59 +0200 Subject: [PATCH 91/92] bump @testing-library (#78270) --- package.json | 13 +- src/dev/jest/setup/react_testing_library.js | 2 +- x-pack/package.json | 11 +- .../apm/public/hooks/useFetcher.test.tsx | 19 +- .../hooks/use_metrics_explorer_data.test.tsx | 15 +- .../user_action_tree/index.test.tsx | 26 +-- .../components/open_timeline/index.test.tsx | 18 +- .../step_define/step_define_form.test.tsx | 5 +- .../action_wizard/action_wizard.test.tsx | 2 +- ...onnected_flyout_manage_drilldowns.test.tsx | 6 +- yarn.lock | 205 +++++++++++------- 11 files changed, 198 insertions(+), 124 deletions(-) diff --git a/package.json b/package.json index 57f5ac16059c9..69df2818bb242 100644 --- a/package.json +++ b/package.json @@ -248,8 +248,11 @@ "@microsoft/api-documenter": "7.7.2", "@microsoft/api-extractor": "7.7.0", "@percy/agent": "^0.26.0", - "@testing-library/react": "^9.3.2", - "@testing-library/react-hooks": "^3.2.1", + "@testing-library/dom": "^7.24.2", + "@testing-library/jest-dom": "^5.11.4", + "@testing-library/react": "^11.0.4", + "@testing-library/react-hooks": "^3.4.1", + "@testing-library/user-event": "^12.1.6", "@types/accept": "3.1.1", "@types/angular": "^1.6.56", "@types/angular-mocks": "^1.7.0", @@ -329,10 +332,8 @@ "@types/supertest": "^2.0.5", "@types/supertest-as-promised": "^2.0.38", "@types/tar": "^4.0.3", - "@types/testing-library__dom": "^6.10.0", - "@types/testing-library__jest-dom": "^5.7.0", - "@types/testing-library__react": "^9.1.2", - "@types/testing-library__react-hooks": "^3.1.0", + "@types/testing-library__jest-dom": "^5.9.2", + "@types/testing-library__react-hooks": "^3.4.0", "@types/type-detect": "^4.0.1", "@types/uuid": "^3.4.4", "@types/vinyl": "^2.0.4", diff --git a/src/dev/jest/setup/react_testing_library.js b/src/dev/jest/setup/react_testing_library.js index 41f58354844a3..84b5b6096e79b 100644 --- a/src/dev/jest/setup/react_testing_library.js +++ b/src/dev/jest/setup/react_testing_library.js @@ -29,4 +29,4 @@ import '@testing-library/jest-dom'; import { configure } from '@testing-library/react/pure'; // instead of default 'data-testid', use kibana's 'data-test-subj' -configure({ testIdAttribute: 'data-test-subj' }); +configure({ testIdAttribute: 'data-test-subj', asyncUtilTimeout: 4500 }); diff --git a/x-pack/package.json b/x-pack/package.json index 3af97ed16ed6f..806b4cd5e2ee8 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -50,9 +50,11 @@ "@storybook/addon-storyshots": "^5.3.19", "@storybook/react": "^5.3.19", "@storybook/theming": "^5.3.19", - "@testing-library/jest-dom": "^5.8.0", - "@testing-library/react": "^9.3.2", - "@testing-library/react-hooks": "^3.2.1", + "@testing-library/dom": "^7.24.2", + "@testing-library/jest-dom": "^5.11.4", + "@testing-library/react": "^11.0.4", + "@testing-library/react-hooks": "^3.4.1", + "@testing-library/user-event": "^12.1.6", "@turf/bbox": "6.0.1", "@turf/bbox-polygon": "6.0.1", "@turf/boolean-contains": "6.0.1", @@ -126,7 +128,8 @@ "@types/styled-components": "^5.1.0", "@types/supertest": "^2.0.5", "@types/tar-fs": "^1.16.1", - "@types/testing-library__jest-dom": "^5.7.0", + "@types/testing-library__jest-dom": "^5.9.2", + "@types/testing-library__react-hooks": "^3.4.0", "@types/tinycolor2": "^1.4.1", "@types/use-resize-observer": "^6.0.0", "@types/uuid": "^3.4.4", diff --git a/x-pack/plugins/apm/public/hooks/useFetcher.test.tsx b/x-pack/plugins/apm/public/hooks/useFetcher.test.tsx index 2db4659c83603..59dd9455c724c 100644 --- a/x-pack/plugins/apm/public/hooks/useFetcher.test.tsx +++ b/x-pack/plugins/apm/public/hooks/useFetcher.test.tsx @@ -4,17 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook, RenderHookResult } from '@testing-library/react-hooks'; import { delay } from '../utils/testHelpers'; -import { useFetcher } from './useFetcher'; +import { FetcherResult, useFetcher } from './useFetcher'; import { MockApmPluginContextWrapper } from '../context/ApmPluginContext/MockApmPluginContext'; +import { ApmPluginContextValue } from '../context/ApmPluginContext'; // Wrap the hook with a provider so it can useApmPluginContext const wrapper = MockApmPluginContextWrapper; describe('useFetcher', () => { describe('when resolving after 500ms', () => { - let hook: ReturnType; + let hook: RenderHookResult< + { children?: React.ReactNode; value?: ApmPluginContextValue }, + FetcherResult & { + refetch: () => void; + } + >; beforeEach(() => { jest.useFakeTimers(); async function fn() { @@ -58,7 +64,12 @@ describe('useFetcher', () => { }); describe('when throwing after 500ms', () => { - let hook: ReturnType; + let hook: RenderHookResult< + { children?: React.ReactNode; value?: ApmPluginContextValue }, + FetcherResult & { + refetch: () => void; + } + >; beforeEach(() => { jest.useFakeTimers(); async function fn() { diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx index b33fe5c232f01..f566e5253c615 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx @@ -18,6 +18,10 @@ import { resp, createSeries, } from '../../../../utils/fixtures/metrics_explorer'; +import { MetricsExplorerOptions, MetricsExplorerTimeOptions } from './use_metrics_explorer_options'; +import { SourceQuery } from '../../../../../common/graphql/types'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; +import { HttpHandler } from 'kibana/public'; const mockedFetch = jest.fn(); @@ -31,7 +35,16 @@ const renderUseMetricsExplorerDataHook = () => { return {children}; }; return renderHook( - (props) => + (props: { + options: MetricsExplorerOptions; + source: SourceQuery.Query['source']['configuration'] | undefined; + derivedIndexPattern: IIndexPattern; + timeRange: MetricsExplorerTimeOptions; + afterKey: string | null | Record; + signal: any; + fetch?: HttpHandler; + shouldLoadImmediately?: boolean; + }) => useMetricsExplorerData( props.options, props.source, diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx index d2bb2fb243458..0b376f26a1ae0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx @@ -6,8 +6,7 @@ import React from 'react'; import { mount } from 'enzyme'; -// we don't have the types for waitFor just yet, so using "as waitFor" until when we do -import { wait as waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; @@ -364,12 +363,12 @@ describe('UserActionTree ', () => { await waitFor(() => { wrapper.update(); - }); - wrapper - .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-quote"]`) - .first() - .simulate('click'); + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-quote"]`) + .first() + .simulate('click'); + }); expect(setFieldValue).toBeCalledWith('comment', `> ${props.data.description} \n`); }); @@ -396,14 +395,13 @@ describe('UserActionTree ', () => { await waitFor(() => { wrapper.update(); + expect( + wrapper + .find(`[data-test-subj="comment-create-action-${commentId}"]`) + .first() + .hasClass('outlined') + ).toBeTruthy(); }); - - expect( - wrapper - .find(`[data-test-subj="comment-create-action-${commentId}"]`) - .first() - .hasClass('outlined') - ).toBeTruthy(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx index facdc392ff7ba..64b9db59467e1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx @@ -10,8 +10,7 @@ import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; import { mount } from 'enzyme'; import { MockedProvider } from 'react-apollo/test-utils'; -// we don't have the types for waitFor just yet, so using "as waitFor" until when we do -import { wait as waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import { useHistory, useParams } from 'react-router-dom'; import '../../../common/mock/match_media'; @@ -533,18 +532,15 @@ describe('StatefulOpenTimeline', () => { ); - await waitFor(() => { - wrapper.update(); + wrapper.update(); - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('itemIdToExpandedNotesRowMap') - ).toEqual({}); + expect( + wrapper.find('[data-test-subj="open-timeline"]').last().prop('itemIdToExpandedNotesRowMap') + ).toEqual({}); - wrapper.find('[data-test-subj="expand-notes"]').first().simulate('click'); + wrapper.find('[data-test-subj="expand-notes"]').first().simulate('click'); + await waitFor(() => { expect( wrapper .find('[data-test-subj="open-timeline"]') diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx index 986ac0a212e8a..d6526fd1db05e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx @@ -66,7 +66,7 @@ describe('Transform: ', () => { storage: createMockStorage(), }; - const { getByLabelText } = render( + const { getByText } = render( @@ -76,7 +76,8 @@ describe('Transform: ', () => { // Act // Assert - expect(getByLabelText('Index pattern')).toBeInTheDocument(); + expect(getByText('Index pattern')).toBeInTheDocument(); + expect(getByText(searchItems.indexPattern.title)).toBeInTheDocument(); await wait(); done(); }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx index fcea8caf9090e..26033b7f020ad 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.test.tsx @@ -80,7 +80,7 @@ test('If not enough license, button is disabled', () => { // check that all factories are displayed to pick expect(screen.getAllByTestId(new RegExp(TEST_SUBJ_ACTION_FACTORY_ITEM))).toHaveLength(2); - expect(screen.getByText(/Go to URL/i)).toBeDisabled(); + expect(screen.getByTestId(/actionFactoryItem-Url/i)).toBeDisabled(); }); test('if action is beta, beta badge is shown', () => { diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx index c4b07fa05c3c1..a546fabfbbc01 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx @@ -56,7 +56,8 @@ test('Allows to manage drilldowns', async () => { fireEvent.click(screen.getByText(/Create new/i)); - let [createHeading, createButton] = screen.getAllByText(/Create Drilldown/i); + let [createHeading] = screen.getAllByText(/Create Drilldown/i); + let createButton = screen.getByRole('button', { name: /Create Drilldown/i }); expect(createHeading).toBeVisible(); expect(screen.getByLabelText(/Back/i)).toBeVisible(); @@ -77,7 +78,8 @@ test('Allows to manage drilldowns', async () => { target: { value: URL }, }); - [createHeading, createButton] = screen.getAllByText(/Create Drilldown/i); + [createHeading] = screen.getAllByText(/Create Drilldown/i); + createButton = screen.getByRole('button', { name: /Create Drilldown/i }); expect(createButton).toBeEnabled(); fireEvent.click(createButton); diff --git a/yarn.lock b/yarn.lock index 3549c79970bff..afb302e17fd2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -990,6 +990,14 @@ core-js "^2.6.5" regenerator-runtime "^0.13.4" +"@babel/runtime-corejs3@^7.10.2": + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.11.2.tgz#02c3029743150188edeb66541195f54600278419" + integrity sha512-qh5IR+8VgFz83VBa6OkaET6uN/mJOhHONuy3m1sgF0CV6mXdPSEBdA7e1eUbVvyNtANjMbg22JUv71BaDXLY6A== + dependencies: + core-js-pure "^3.0.0" + regenerator-runtime "^0.13.4" + "@babel/runtime@7.3.4": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" @@ -997,7 +1005,7 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.11.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== @@ -1844,6 +1852,17 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" +"@jest/types@^26.3.0": + version "26.3.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.3.0.tgz#97627bf4bdb72c55346eef98e3b3f7ddc4941f71" + integrity sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + "@jimp/bmp@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@jimp/bmp/-/bmp-0.14.0.tgz#6df246026554f276f7b354047c6fff9f5b2b5182" @@ -2720,11 +2739,6 @@ dependencies: url-pattern "^1.0.3" -"@sheerun/mutationobserver-shim@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b" - integrity sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q== - "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -3342,49 +3356,55 @@ resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.0.7.tgz#0cd915785ec4190f08a3a6acc9b61fc38fb5f1a9" integrity sha512-8UT/J+xqCYfn3fKtOznAibsHpiuDshCb0fwgWxRazTT19Igp9ovoXMPhXyLD6m3CKQGTMHgqoxaFfMWaL40Rnw== -"@testing-library/dom@^6.3.0": - version "6.10.1" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-6.10.1.tgz#da5bf5065d3f9e484aef4cc495f4e1a5bea6df2e" - integrity sha512-5BPKxaO+zSJDUbVZBRNf9KrmDkm/EcjjaHSg3F9+031VZyPACKXlwLBjVzZxheunT9m72DoIq7WvyE457/Xweg== +"@testing-library/dom@^7.24.2": + version "7.24.2" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.24.2.tgz#6d2b7dd21efbd5358b98c2777fc47c252f3ae55e" + integrity sha512-ERxcZSoHx0EcN4HfshySEWmEf5Kkmgi+J7O79yCJ3xggzVlBJ2w/QjJUC+EBkJJ2OeSw48i3IoePN4w8JlVUIA== dependencies: - "@babel/runtime" "^7.6.2" - "@sheerun/mutationobserver-shim" "^0.3.2" - "@types/testing-library__dom" "^6.0.0" - aria-query "3.0.0" - pretty-format "^24.9.0" - wait-for-expect "^3.0.0" + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.10.3" + "@types/aria-query" "^4.2.0" + aria-query "^4.2.2" + chalk "^4.1.0" + dom-accessibility-api "^0.5.1" + pretty-format "^26.4.2" -"@testing-library/jest-dom@^5.8.0": - version "5.8.0" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.8.0.tgz#815e830129c4dda6c8e9a725046397acec523669" - integrity sha512-9Y4FxYIxfwHpUyJVqI8EOfDP2LlEBqKwXE3F+V8ightji0M2rzQB+9kqZ5UJxNs+9oXJIgvYj7T3QaXLNHVDMw== +"@testing-library/jest-dom@^5.11.4": + version "5.11.4" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.11.4.tgz#f325c600db352afb92995c2576022b35621ddc99" + integrity sha512-6RRn3epuweBODDIv3dAlWjOEHQLpGJHB2i912VS3JQtsD22+ENInhdDNl4ZZQiViLlIfFinkSET/J736ytV9sw== dependencies: "@babel/runtime" "^7.9.2" - "@types/testing-library__jest-dom" "^5.0.2" + "@types/testing-library__jest-dom" "^5.9.1" + aria-query "^4.2.2" chalk "^3.0.0" - css "^2.2.4" + css "^3.0.0" css.escape "^1.5.1" - jest-diff "^25.1.0" - jest-matcher-utils "^25.1.0" lodash "^4.17.15" redent "^3.0.0" -"@testing-library/react-hooks@^3.2.1": - version "3.2.1" - resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-3.2.1.tgz#19b6caa048ef15faa69d439c469033873ea01294" - integrity sha512-1OB6Ksvlk6BCJA1xpj8/WWz0XVd1qRcgqdaFAq+xeC6l61Ucj0P6QpA5u+Db/x9gU4DCX8ziR5b66Mlfg0M2RA== +"@testing-library/react-hooks@^3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-3.4.1.tgz#1f8ccd21208086ec228d9743fe40b69d0efcd7e5" + integrity sha512-LbzvE7oKsVzuW1cxA/aOeNgeVvmHWG2p/WSzalIGyWuqZT3jVcNDT5KPEwy36sUYWde0Qsh32xqIUFXukeywXg== dependencies: "@babel/runtime" "^7.5.4" - "@types/testing-library__react-hooks" "^3.0.0" + "@types/testing-library__react-hooks" "^3.3.0" -"@testing-library/react@^9.3.2": - version "9.3.2" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-9.3.2.tgz#418000daa980dafd2d9420cc733d661daece9aa0" - integrity sha512-J6ftWtm218tOLS175MF9eWCxGp+X+cUXCpkPIin8KAXWtyZbr9CbqJ8M8QNd6spZxJDAGlw+leLG4MJWLlqVgg== +"@testing-library/react@^11.0.4": + version "11.0.4" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.0.4.tgz#c84082bfe1593d8fcd475d46baee024452f31dee" + integrity sha512-U0fZO2zxm7M0CB5h1+lh31lbAwMSmDMEMGpMT3BUPJwIjDEKYWOV4dx7lb3x2Ue0Pyt77gmz/VropuJnSz/Iew== + dependencies: + "@babel/runtime" "^7.11.2" + "@testing-library/dom" "^7.24.2" + +"@testing-library/user-event@^12.1.6": + version "12.1.6" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-12.1.6.tgz#f550b138dfdc20387b89cbe3e9f3d969ab10c2bd" + integrity sha512-BdSe6cmzDEapTBH3s1NKbzu+GyX5bJKraKwVpM2vZF1+EEWxZr0EiA0z9bA5Nux8P+6nKMOZKsXQrj5q/kicfQ== dependencies: - "@babel/runtime" "^7.6.0" - "@testing-library/dom" "^6.3.0" - "@types/testing-library__react" "^9.1.0" + "@babel/runtime" "^7.10.2" "@turf/bbox-polygon@6.0.1": version "6.0.1" @@ -3510,6 +3530,11 @@ resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-1.0.33.tgz#2728669427cdd74a99e53c9f457ca2866a37c52d" integrity sha512-VQgHxyPMTj3hIlq9SY1mctqx+Jj8kpQfoLvDlVSDNOyuYs8JYfkuY3OW/4+dO657yPmNhHpePRx0/Tje5ImNVQ== +"@types/aria-query@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.0.tgz#14264692a9d6e2fa4db3df5e56e94b5e25647ac0" + integrity sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A== + "@types/async@2.0.49": version "2.0.49" resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.49.tgz#92e33d13f74c895cb9a7f38ba97db8431ed14bc0" @@ -4105,6 +4130,13 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/istanbul-reports@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz#508b13aa344fa4976234e75dddcc34925737d821" + integrity sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA== + dependencies: + "@types/istanbul-lib-report" "*" + "@types/jest-specific-snapshot@^0.5.3", "@types/jest-specific-snapshot@^0.5.4": version "0.5.4" resolved "https://registry.yarnpkg.com/@types/jest-specific-snapshot/-/jest-specific-snapshot-0.5.4.tgz#997364c39a59ddeff0ee790a19415e79dd061d1e" @@ -4564,7 +4596,7 @@ dependencies: "@types/react" "*" -"@types/react-dom@*", "@types/react-dom@^16.9.8": +"@types/react-dom@^16.9.8": version "16.9.8" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423" integrity sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA== @@ -4880,43 +4912,20 @@ resolved "https://registry.yarnpkg.com/@types/tempy/-/tempy-0.2.0.tgz#8b7a93f6912aef25cc0b8d8a80ff974151478685" integrity sha512-YaX74QljqR45Xu7dd22wMvzTS+ItUiSyDl9XJl6WTgYNE09r2TF+mV2FDjWRM5Sdzf9C9dXRTUdz9J5SoEYxXg== -"@types/testing-library__dom@*", "@types/testing-library__dom@^6.0.0": - version "6.10.0" - resolved "https://registry.yarnpkg.com/@types/testing-library__dom/-/testing-library__dom-6.10.0.tgz#590d76e3875a7c536dc744eb530cbf51b6483404" - integrity sha512-mL/GMlyQxiZplbUuFNwA0vAI3k3uJNSf6slr5AVve9TXmfLfyefNT0uHHnxwdYuPMxYD5gI/+dgAvc/5opW9JQ== - dependencies: - pretty-format "^24.3.0" - -"@types/testing-library__dom@^6.10.0": - version "6.14.0" - resolved "https://registry.yarnpkg.com/@types/testing-library__dom/-/testing-library__dom-6.14.0.tgz#1aede831cb4ed4a398448df5a2c54b54a365644e" - integrity sha512-sMl7OSv0AvMOqn1UJ6j1unPMIHRXen0Ita1ujnMX912rrOcawe4f7wu0Zt9GIQhBhJvH2BaibqFgQ3lP+Pj2hA== - dependencies: - pretty-format "^24.3.0" - -"@types/testing-library__jest-dom@^5.0.2", "@types/testing-library__jest-dom@^5.7.0": - version "5.7.0" - resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.7.0.tgz#078790bf4dc89152a74428591a228ec5f9433251" - integrity sha512-LoZ3uonlnAbJUz4bg6UoeFl+frfndXngmkCItSjJ8DD5WlRfVqPC5/LgJASsY/dy7AHH2YJ7PcsdASOydcVeFA== +"@types/testing-library__jest-dom@^5.9.1", "@types/testing-library__jest-dom@^5.9.2": + version "5.9.2" + resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.2.tgz#59e4771a1cf87d51e89a5cc8195cd3b647cba322" + integrity sha512-K7nUSpH/5i8i0NagTJ+uFUDRueDlnMNhJtMjMwTGPPSqyImbWC/hgKPDCKt6Phu2iMJg2kWqlax+Ucj2DKMwpA== dependencies: "@types/jest" "*" -"@types/testing-library__react-hooks@^3.0.0", "@types/testing-library__react-hooks@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.1.0.tgz#04d174ce767fbcce3ccb5021d7f156e1b06008a9" - integrity sha512-QJc1sgH9DD6jbfybzugnP0sY8wPzzIq8sHDBuThzCr2ZEbyHIaAvN9ytx/tHzcWL5MqmeZJqiUm/GsythaGx3g== +"@types/testing-library__react-hooks@^3.3.0", "@types/testing-library__react-hooks@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.4.0.tgz#be148b7fa7d19cd3349c4ef9d9534486bc582fcc" + integrity sha512-QYLZipqt1hpwYsBU63Ssa557v5wWbncqL36No59LI7W3nCMYKrLWTnYGn2griZ6v/3n5nKXNYkTeYpqPHY7Ukg== dependencies: - "@types/react" "*" "@types/react-test-renderer" "*" -"@types/testing-library__react@^9.1.0", "@types/testing-library__react@^9.1.2": - version "9.1.2" - resolved "https://registry.yarnpkg.com/@types/testing-library__react/-/testing-library__react-9.1.2.tgz#e33af9124c60a010fc03a34eff8f8a34a75c4351" - integrity sha512-CYaMqrswQ+cJACy268jsLAw355DZtPZGt3Jwmmotlcu8O/tkoXBI6AeZ84oZBJsIsesozPKzWzmv/0TIU+1E9Q== - dependencies: - "@types/react-dom" "*" - "@types/testing-library__dom" "*" - "@types/through@*": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" @@ -6291,7 +6300,7 @@ aria-hidden@^1.1.1: dependencies: tslib "^1.0.0" -aria-query@3.0.0, aria-query@^3.0.0: +aria-query@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc" integrity sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w= @@ -6299,6 +6308,14 @@ aria-query@3.0.0, aria-query@^3.0.0: ast-types-flow "0.0.7" commander "^2.11.0" +aria-query@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" + integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== + dependencies: + "@babel/runtime" "^7.10.2" + "@babel/runtime-corejs3" "^7.10.2" + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -6702,7 +6719,7 @@ atob-lite@^2.0.0: resolved "https://registry.yarnpkg.com/atob-lite/-/atob-lite-2.0.0.tgz#0fef5ad46f1bd7a8502c65727f0367d5ee43d696" integrity sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY= -atob@^2.1.1: +atob@^2.1.1, atob@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== @@ -9572,6 +9589,11 @@ core-js-compat@^3.6.2: browserslist "^4.8.3" semver "7.0.0" +core-js-pure@^3.0.0: + version "3.6.5" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" + integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== + core-js-pure@^3.0.1: version "3.2.1" resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.2.1.tgz#879a23699cff46175bfd2d09158b5c50645a3c45" @@ -9995,6 +10017,15 @@ css@2.X, css@^2.2.1, css@^2.2.4: source-map-resolve "^0.5.2" urix "^0.1.0" +css@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/css/-/css-3.0.0.tgz#4447a4d58fdd03367c516ca9f64ae365cee4aa5d" + integrity sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ== + dependencies: + inherits "^2.0.4" + source-map "^0.6.1" + source-map-resolve "^0.6.0" + csscolorparser@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b" @@ -11086,6 +11117,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.2.tgz#ef3cdb5d3f0d599d8f9c8b18df2fb63c9793739d" + integrity sha512-k7hRNKAiPJXD2aBqfahSo4/01cTsKWXf+LqJgglnkN2Nz8TsxXKQBXHhKe0Ye9fEfHEZY49uSA5Sr3AqP/sWKA== + dom-converter@~0.2: version "0.2.0" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" @@ -17395,7 +17431,7 @@ jest-diff@^24.3.0, jest-diff@^24.9.0: jest-get-type "^24.9.0" pretty-format "^24.9.0" -jest-diff@^25.1.0, jest-diff@^25.2.1, jest-diff@^25.5.0: +jest-diff@^25.2.1, jest-diff@^25.5.0: version "25.5.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.5.0.tgz#1dd26ed64f96667c068cef026b677dfa01afcfa9" integrity sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A== @@ -17546,7 +17582,7 @@ jest-matcher-utils@^24.9.0: jest-get-type "^24.9.0" pretty-format "^24.9.0" -jest-matcher-utils@^25.1.0, jest-matcher-utils@^25.5.0: +jest-matcher-utils@^25.5.0: version "25.5.0" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz#fbc98a12d730e5d2453d7f1ed4a4d948e34b7867" integrity sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw== @@ -22815,7 +22851,7 @@ pretty-error@^2.1.1: renderkid "^2.0.1" utila "~0.4" -pretty-format@^24.3.0, pretty-format@^24.9.0: +pretty-format@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9" integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA== @@ -22835,6 +22871,16 @@ pretty-format@^25.2.1, pretty-format@^25.5.0: ansi-styles "^4.0.0" react-is "^16.12.0" +pretty-format@^26.4.2: + version "26.4.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.4.2.tgz#d081d032b398e801e2012af2df1214ef75a81237" + integrity sha512-zK6Gd8zDsEiVydOCGLkoBoZuqv8VTiHyAbKznXe/gaph/DAeZOmit9yMfgIz5adIgAMMs5XfoYSwAX3jcCO1tA== + dependencies: + "@jest/types" "^26.3.0" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^16.12.0" + pretty-hrtime@^1.0.0, pretty-hrtime@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" @@ -26342,6 +26388,14 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: source-map-url "^0.4.0" urix "^0.1.0" +source-map-resolve@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2" + integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + source-map-support@^0.3.2: version "0.3.3" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.3.3.tgz#34900977d5ba3f07c7757ee72e73bb1a9b53754f" @@ -29887,11 +29941,6 @@ w3c-xmlserializer@^1.0.1, w3c-xmlserializer@^1.1.2: webidl-conversions "^4.0.2" xml-name-validator "^3.0.0" -wait-for-expect@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-3.0.1.tgz#ec204a76b0038f17711e575720aaf28505ac7185" - integrity sha512-3Ha7lu+zshEG/CeHdcpmQsZnnZpPj/UsG3DuKO8FskjuDbkx3jE3845H+CuwZjA2YWYDfKMU2KhnCaXMLd3wVw== - walk@2.3.x: version "2.3.9" resolved "https://registry.yarnpkg.com/walk/-/walk-2.3.9.tgz#31b4db6678f2ae01c39ea9fb8725a9031e558a7b" From 18f7f042c1b6bd36c0cf09c9fed4396e7484e0a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 24 Sep 2020 14:05:19 +0100 Subject: [PATCH 92/92] [Usage Collection] Add schema to `stack_management` (#77897) Co-authored-by: Elastic Machine --- .telemetryrc.json | 1 - .../src/tools/check_collector_integrity.ts | 2 + .../server/collectors/management/schema.ts | 116 ++++++++ .../telemetry_management_collector.ts | 10 +- src/plugins/telemetry/schema/oss_plugins.json | 271 ++++++++++++++++++ 5 files changed, 397 insertions(+), 3 deletions(-) create mode 100644 src/plugins/kibana_usage_collection/server/collectors/management/schema.ts diff --git a/.telemetryrc.json b/.telemetryrc.json index 7d9743b20ff68..d3446b45033ee 100644 --- a/.telemetryrc.json +++ b/.telemetryrc.json @@ -6,7 +6,6 @@ "src/plugins/kibana_react/", "src/plugins/testbed/", "src/plugins/kibana_utils/", - "src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts", "src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts" ] } diff --git a/packages/kbn-telemetry-tools/src/tools/check_collector_integrity.ts b/packages/kbn-telemetry-tools/src/tools/check_collector_integrity.ts index 3205edb87aa29..8a5752f77d7fc 100644 --- a/packages/kbn-telemetry-tools/src/tools/check_collector_integrity.ts +++ b/packages/kbn-telemetry-tools/src/tools/check_collector_integrity.ts @@ -47,6 +47,7 @@ export function checkCompatibleTypeDescriptor( const typeDescriptorKinds = reduce( typeDescriptorTypes, (acc: any, type: number, key: string) => { + key = key.replace(/'/g, ''); try { acc[key] = kindToDescriptorName(type); } catch (err) { @@ -61,6 +62,7 @@ export function checkCompatibleTypeDescriptor( const transformedMappingKinds = reduce( schemaTypes, (acc: any, type: string, key: string) => { + key = key.replace(/'/g, ''); try { acc[key.replace(/.type$/, '.kind')] = compatibleSchemaTypes(type as any); } catch (err) { diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts new file mode 100644 index 0000000000000..792ac24b4de3d --- /dev/null +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -0,0 +1,116 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MakeSchemaFrom } from 'src/plugins/usage_collection/server'; +import { UsageStats } from './telemetry_management_collector'; + +// Retrieved by changing all the current settings in Kibana (we'll need to revisit it in the future). +// I would suggest we use flattened type for the mappings of this collector. +export const stackManagementSchema: MakeSchemaFrom = { + 'visualize:enableLabs': { type: 'boolean' }, + 'visualization:heatmap:maxBuckets': { type: 'long' }, + 'visualization:colorMapping': { type: 'text' }, + 'visualization:regionmap:showWarnings': { type: 'boolean' }, + 'visualization:dimmingOpacity': { type: 'float' }, + 'visualization:tileMap:maxPrecision': { type: 'long' }, + 'securitySolution:ipReputationLinks': { type: 'text' }, + 'csv:separator': { type: 'keyword' }, + 'visualization:tileMap:WMSdefaults': { type: 'text' }, + 'timelion:target_buckets': { type: 'long' }, + 'timelion:max_buckets': { type: 'long' }, + 'timelion:es.timefield': { type: 'keyword' }, + 'timelion:min_interval': { type: 'keyword' }, + 'timelion:default_rows': { type: 'long' }, + 'timelion:default_columns': { type: 'long' }, + 'timelion:quandl.key': { type: 'keyword' }, + 'timelion:es.default_index': { type: 'keyword' }, + 'timelion:showTutorial': { type: 'boolean' }, + 'securitySolution:timeDefaults': { type: 'keyword' }, + 'securitySolution:defaultAnomalyScore': { type: 'long' }, + 'securitySolution:defaultIndex': { type: 'keyword' }, // it's an array + 'securitySolution:refreshIntervalDefaults': { type: 'keyword' }, + 'securitySolution:newsFeedUrl': { type: 'keyword' }, + 'securitySolution:enableNewsFeed': { type: 'boolean' }, + 'search:includeFrozen': { type: 'boolean' }, + 'courier:maxConcurrentShardRequests': { type: 'long' }, + 'courier:batchSearches': { type: 'boolean' }, + 'courier:setRequestPreference': { type: 'keyword' }, + 'courier:customRequestPreference': { type: 'keyword' }, + 'courier:ignoreFilterIfFieldNotInIndex': { type: 'boolean' }, + 'rollups:enableIndexPatterns': { type: 'boolean' }, + 'xpackReporting:customPdfLogo': { type: 'text' }, + 'notifications:lifetime:warning': { type: 'long' }, + 'notifications:lifetime:banner': { type: 'long' }, + 'notifications:lifetime:info': { type: 'long' }, + 'notifications:banner': { type: 'text' }, + 'notifications:lifetime:error': { type: 'long' }, + 'doc_table:highlight': { type: 'boolean' }, + 'discover:searchOnPageLoad': { type: 'boolean' }, + // eslint-disable-next-line @typescript-eslint/naming-convention + 'doc_table:hideTimeColumn': { type: 'boolean' }, + 'discover:sampleSize': { type: 'long' }, + defaultColumns: { type: 'keyword' }, // it's an array + 'context:defaultSize': { type: 'long' }, + 'discover:aggs:terms:size': { type: 'long' }, + 'context:tieBreakerFields': { type: 'keyword' }, // it's an array + 'discover:sort:defaultOrder': { type: 'keyword' }, + 'context:step': { type: 'long' }, + 'accessibility:disableAnimations': { type: 'boolean' }, + 'ml:fileDataVisualizerMaxFileSize': { type: 'keyword' }, + 'ml:anomalyDetection:results:enableTimeDefaults': { type: 'boolean' }, + 'ml:anomalyDetection:results:timeDefaults': { type: 'keyword' }, + 'truncate:maxHeight': { type: 'long' }, + 'timepicker:timeDefaults': { type: 'keyword' }, + 'timepicker:refreshIntervalDefaults': { type: 'keyword' }, + 'timepicker:quickRanges': { type: 'keyword' }, + 'theme:version': { type: 'keyword' }, + 'theme:darkMode': { type: 'boolean' }, + 'state:storeInSessionStorage': { type: 'boolean' }, + 'savedObjects:perPage': { type: 'long' }, + 'search:queryLanguage': { type: 'keyword' }, + 'shortDots:enable': { type: 'boolean' }, + 'sort:options': { type: 'keyword' }, + 'savedObjects:listingLimit': { type: 'long' }, + 'query:queryString:options': { type: 'keyword' }, + pageNavigation: { type: 'keyword' }, + 'metrics:max_buckets': { type: 'long' }, + 'query:allowLeadingWildcards': { type: 'boolean' }, + metaFields: { type: 'keyword' }, // it's an array + 'indexPattern:placeholder': { type: 'keyword' }, + 'histogram:barTarget': { type: 'long' }, + 'histogram:maxBars': { type: 'long' }, + 'format:number:defaultLocale': { type: 'keyword' }, + 'format:percent:defaultPattern': { type: 'keyword' }, + 'format:number:defaultPattern': { type: 'keyword' }, + 'history:limit': { type: 'long' }, + 'format:defaultTypeMap': { type: 'keyword' }, + 'format:currency:defaultPattern': { type: 'keyword' }, + defaultIndex: { type: 'keyword' }, + 'format:bytes:defaultPattern': { type: 'keyword' }, + 'filters:pinnedByDefault': { type: 'boolean' }, + 'filterEditor:suggestValues': { type: 'boolean' }, + 'fields:popularLimit': { type: 'long' }, + dateNanosFormat: { type: 'keyword' }, + defaultRoute: { type: 'keyword' }, + 'dateFormat:tz': { type: 'keyword' }, + 'dateFormat:scaled': { type: 'keyword' }, + 'csv:quoteValues': { type: 'boolean' }, + 'dateFormat:dow': { type: 'keyword' }, + dateFormat: { type: 'keyword' }, +}; diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts index 3a777beebd90a..612b1714020ef 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/telemetry_management_collector.ts @@ -19,8 +19,13 @@ import { IUiSettingsClient } from 'kibana/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { stackManagementSchema } from './schema'; -export type UsageStats = Record; +export interface UsageStats extends Record { + // We don't support `type` yet. Only interfaces. So I added at least 1 known key to the generic + // Record extension to avoid eslint reverting it back to a `type` + 'visualize:enableLabs': boolean; +} export function createCollectorFetch(getUiSettingsClient: () => IUiSettingsClient | undefined) { return async function fetchUsageStats(): Promise { @@ -45,10 +50,11 @@ export function registerManagementUsageCollector( usageCollection: UsageCollectionSetup, getUiSettingsClient: () => IUiSettingsClient | undefined ) { - const collector = usageCollection.makeUsageCollector({ + const collector = usageCollection.makeUsageCollector({ type: 'stack_management', isReady: () => typeof getUiSettingsClient() !== 'undefined', fetch: createCollectorFetch(getUiSettingsClient), + schema: stackManagementSchema, }); usageCollection.registerCollector(collector); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index a83cd5a562ff6..3ee0c181203aa 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -1346,6 +1346,277 @@ } } }, + "stack_management": { + "properties": { + "visualize:enableLabs": { + "type": "boolean" + }, + "visualization:heatmap:maxBuckets": { + "type": "long" + }, + "visualization:colorMapping": { + "type": "text" + }, + "visualization:regionmap:showWarnings": { + "type": "boolean" + }, + "visualization:dimmingOpacity": { + "type": "float" + }, + "visualization:tileMap:maxPrecision": { + "type": "long" + }, + "securitySolution:ipReputationLinks": { + "type": "text" + }, + "csv:separator": { + "type": "keyword" + }, + "visualization:tileMap:WMSdefaults": { + "type": "text" + }, + "timelion:target_buckets": { + "type": "long" + }, + "timelion:max_buckets": { + "type": "long" + }, + "timelion:es.timefield": { + "type": "keyword" + }, + "timelion:min_interval": { + "type": "keyword" + }, + "timelion:default_rows": { + "type": "long" + }, + "timelion:default_columns": { + "type": "long" + }, + "timelion:quandl.key": { + "type": "keyword" + }, + "timelion:es.default_index": { + "type": "keyword" + }, + "timelion:showTutorial": { + "type": "boolean" + }, + "securitySolution:timeDefaults": { + "type": "keyword" + }, + "securitySolution:defaultAnomalyScore": { + "type": "long" + }, + "securitySolution:defaultIndex": { + "type": "keyword" + }, + "securitySolution:refreshIntervalDefaults": { + "type": "keyword" + }, + "securitySolution:newsFeedUrl": { + "type": "keyword" + }, + "securitySolution:enableNewsFeed": { + "type": "boolean" + }, + "search:includeFrozen": { + "type": "boolean" + }, + "courier:maxConcurrentShardRequests": { + "type": "long" + }, + "courier:batchSearches": { + "type": "boolean" + }, + "courier:setRequestPreference": { + "type": "keyword" + }, + "courier:customRequestPreference": { + "type": "keyword" + }, + "courier:ignoreFilterIfFieldNotInIndex": { + "type": "boolean" + }, + "rollups:enableIndexPatterns": { + "type": "boolean" + }, + "xpackReporting:customPdfLogo": { + "type": "text" + }, + "notifications:lifetime:warning": { + "type": "long" + }, + "notifications:lifetime:banner": { + "type": "long" + }, + "notifications:lifetime:info": { + "type": "long" + }, + "notifications:banner": { + "type": "text" + }, + "notifications:lifetime:error": { + "type": "long" + }, + "doc_table:highlight": { + "type": "boolean" + }, + "discover:searchOnPageLoad": { + "type": "boolean" + }, + "doc_table:hideTimeColumn": { + "type": "boolean" + }, + "discover:sampleSize": { + "type": "long" + }, + "defaultColumns": { + "type": "keyword" + }, + "context:defaultSize": { + "type": "long" + }, + "discover:aggs:terms:size": { + "type": "long" + }, + "context:tieBreakerFields": { + "type": "keyword" + }, + "discover:sort:defaultOrder": { + "type": "keyword" + }, + "context:step": { + "type": "long" + }, + "accessibility:disableAnimations": { + "type": "boolean" + }, + "ml:fileDataVisualizerMaxFileSize": { + "type": "keyword" + }, + "ml:anomalyDetection:results:enableTimeDefaults": { + "type": "boolean" + }, + "ml:anomalyDetection:results:timeDefaults": { + "type": "keyword" + }, + "truncate:maxHeight": { + "type": "long" + }, + "timepicker:timeDefaults": { + "type": "keyword" + }, + "timepicker:refreshIntervalDefaults": { + "type": "keyword" + }, + "timepicker:quickRanges": { + "type": "keyword" + }, + "theme:version": { + "type": "keyword" + }, + "theme:darkMode": { + "type": "boolean" + }, + "state:storeInSessionStorage": { + "type": "boolean" + }, + "savedObjects:perPage": { + "type": "long" + }, + "search:queryLanguage": { + "type": "keyword" + }, + "shortDots:enable": { + "type": "boolean" + }, + "sort:options": { + "type": "keyword" + }, + "savedObjects:listingLimit": { + "type": "long" + }, + "query:queryString:options": { + "type": "keyword" + }, + "pageNavigation": { + "type": "keyword" + }, + "metrics:max_buckets": { + "type": "long" + }, + "query:allowLeadingWildcards": { + "type": "boolean" + }, + "metaFields": { + "type": "keyword" + }, + "indexPattern:placeholder": { + "type": "keyword" + }, + "histogram:barTarget": { + "type": "long" + }, + "histogram:maxBars": { + "type": "long" + }, + "format:number:defaultLocale": { + "type": "keyword" + }, + "format:percent:defaultPattern": { + "type": "keyword" + }, + "format:number:defaultPattern": { + "type": "keyword" + }, + "history:limit": { + "type": "long" + }, + "format:defaultTypeMap": { + "type": "keyword" + }, + "format:currency:defaultPattern": { + "type": "keyword" + }, + "defaultIndex": { + "type": "keyword" + }, + "format:bytes:defaultPattern": { + "type": "keyword" + }, + "filters:pinnedByDefault": { + "type": "boolean" + }, + "filterEditor:suggestValues": { + "type": "boolean" + }, + "fields:popularLimit": { + "type": "long" + }, + "dateNanosFormat": { + "type": "keyword" + }, + "defaultRoute": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "keyword" + }, + "dateFormat:scaled": { + "type": "keyword" + }, + "csv:quoteValues": { + "type": "boolean" + }, + "dateFormat:dow": { + "type": "keyword" + }, + "dateFormat": { + "type": "keyword" + } + } + }, "telemetry": { "properties": { "opt_in_status": {