diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc
index f62a4d28dfc0df..7081590931a992 100644
--- a/docs/management/advanced-options.asciidoc
+++ b/docs/management/advanced-options.asciidoc
@@ -217,6 +217,8 @@ might increase the search time. This setting is off by default. Users must opt-i
[horizontal]
`siem:defaultAnomalyScore`:: The threshold above which Machine Learning job anomalies are displayed in the SIEM app.
`siem:defaultIndex`:: A comma-delimited list of Elasticsearch indices from which the SIEM app collects events.
+`siem:ipReputationLinks`:: A JSON array containing links for verifying the reputation of an IP address. The links are displayed on
+{siem-guide}/siem-ui-overview.html#network-ui[IP detail] pages.
`siem:enableNewsFeed`:: Enables the security news feed on the SIEM *Overview*
page.
`siem:newsFeedUrl`:: The URL from which the security news feed content is
diff --git a/docs/siem/images/cases-ui.png b/docs/siem/images/cases-ui.png
new file mode 100644
index 00000000000000..b513efb6647407
Binary files /dev/null and b/docs/siem/images/cases-ui.png differ
diff --git a/docs/siem/siem-ui.asciidoc b/docs/siem/siem-ui.asciidoc
index 85253daaf29330..985138756622d2 100644
--- a/docs/siem/siem-ui.asciidoc
+++ b/docs/siem/siem-ui.asciidoc
@@ -35,7 +35,7 @@ image::siem/images/network-ui.png[]
[float]
[[detections-ui]]
-=== Detections (Beta)
+=== Detections (beta)
The Detections feature automatically searches for threats and creates
signals when they are detected. Signal detection rules define the conditions
@@ -50,6 +50,22 @@ or the Detections API.
[role="screenshot"]
image::siem/images/detections-ui.png[]
+[float]
+[[cases-ui]]
+=== Cases (beta)
+
+Cases are used to open and track security issues directly in SIEM.
+Cases list the original reporter and all users who contribute to a case
+(`participants`). Case comments support Markdown syntax, and allow linking to
+saved Timelines. Additionally, you can send cases to external systems from
+within SIEM (currently ServiceNow).
+
+For information about opening, updating, and closing cases, see
+{siem-guide}/cases-overview.html[Cases] in the SIEM Guide.
+
+[role="screenshot"]
+image::siem/images/cases-ui.png[]
+
[float]
[[timelines-ui]]
=== Timeline
diff --git a/packages/kbn-storybook/storybook_config/webpack.config.js b/packages/kbn-storybook/storybook_config/webpack.config.js
index 1531c1d22b01bc..779d8a41536445 100644
--- a/packages/kbn-storybook/storybook_config/webpack.config.js
+++ b/packages/kbn-storybook/storybook_config/webpack.config.js
@@ -49,6 +49,13 @@ module.exports = async ({ config }) => {
},
});
+ config.module.rules.push({
+ test: /\.(html|md|txt|tmpl)$/,
+ use: {
+ loader: 'raw-loader',
+ },
+ });
+
// Handle Typescript files
config.module.rules.push({
test: /\.tsx?$/,
diff --git a/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts b/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts
index 30a93989649a7a..b3ce2f1e57d5fc 100644
--- a/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts
+++ b/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts
@@ -33,6 +33,10 @@ export class PlaceholderEmbeddableFactory implements EmbeddableFactoryDefinition
return false;
}
+ public canCreateNew() {
+ return false;
+ }
+
public async create(initialInput: EmbeddableInput, parent?: IContainer) {
return new PlaceholderEmbeddable(initialInput, parent);
}
diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js
index 9522fd12ad37d1..1b1fbf111fe04b 100644
--- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js
+++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js
@@ -39,6 +39,7 @@ import {
replaceLayerList,
setQuery,
clearTransientLayerStateAndCloseFlyout,
+ setMapSettings,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../plugins/maps/public/actions/map_actions';
import {
@@ -52,10 +53,14 @@ import {
setReadOnly,
setIsLayerTOCOpen,
setOpenTOCDetails,
+ openMapSettings,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../plugins/maps/public/actions/ui_actions';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { getIsFullScreen } from '../../../../../plugins/maps/public/selectors/ui_selectors';
+import {
+ getIsFullScreen,
+ getFlyoutDisplay,
+ // eslint-disable-next-line @kbn/eslint/no-restricted-paths
+} from '../../../../../plugins/maps/public/selectors/ui_selectors';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { copyPersistentState } from '../../../../../plugins/maps/public/reducers/util';
import {
@@ -395,6 +400,9 @@ app.controller(
if (mapState.filters) {
savedObjectFilters = mapState.filters;
}
+ if (mapState.settings) {
+ store.dispatch(setMapSettings(mapState.settings));
+ }
}
if (savedMap.uiStateJSON) {
@@ -453,6 +461,7 @@ app.controller(
$scope.isFullScreen = false;
$scope.isSaveDisabled = false;
+ $scope.isOpenSettingsDisabled = false;
function handleStoreChanges(store) {
const nextIsFullScreen = getIsFullScreen(store.getState());
if (nextIsFullScreen !== $scope.isFullScreen) {
@@ -474,6 +483,14 @@ app.controller(
$scope.isSaveDisabled = nextIsSaveDisabled;
});
}
+
+ const flyoutDisplay = getFlyoutDisplay(store.getState());
+ const nextIsOpenSettingsDisabled = flyoutDisplay !== FLYOUT_STATE.NONE;
+ if (nextIsOpenSettingsDisabled !== $scope.isOpenSettingsDisabled) {
+ $scope.$evalAsync(() => {
+ $scope.isOpenSettingsDisabled = nextIsOpenSettingsDisabled;
+ });
+ }
}
$scope.$on('$destroy', () => {
@@ -591,6 +608,22 @@ app.controller(
getInspector().open(inspectorAdapters, {});
},
},
+ {
+ id: 'mapSettings',
+ label: i18n.translate('xpack.maps.mapController.openSettingsButtonLabel', {
+ defaultMessage: `Map settings`,
+ }),
+ description: i18n.translate('xpack.maps.mapController.openSettingsDescription', {
+ defaultMessage: `Open map settings`,
+ }),
+ testId: 'openSettingsButton',
+ disableButton() {
+ return $scope.isOpenSettingsDisabled;
+ },
+ run() {
+ store.dispatch(openMapSettings());
+ },
+ },
...(getMapsCapabilities().save
? [
{
diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts
index 6e03583dda69fa..d572561944a76d 100644
--- a/x-pack/legacy/plugins/siem/index.ts
+++ b/x-pack/legacy/plugins/siem/index.ts
@@ -8,27 +8,7 @@ import { i18n } from '@kbn/i18n';
import { resolve } from 'path';
import { Root } from 'joi';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { savedObjectMappings } from '../../../plugins/siem/server/saved_objects';
-
-import {
- APP_ID,
- APP_NAME,
- DEFAULT_INDEX_KEY,
- DEFAULT_ANOMALY_SCORE,
- DEFAULT_SIEM_TIME_RANGE,
- DEFAULT_SIEM_REFRESH_INTERVAL,
- DEFAULT_INTERVAL_PAUSE,
- DEFAULT_INTERVAL_VALUE,
- DEFAULT_FROM,
- DEFAULT_TO,
- ENABLE_NEWS_FEED_SETTING,
- NEWS_FEED_URL_SETTING,
- NEWS_FEED_URL_SETTING_DEFAULT,
- IP_REPUTATION_LINKS_SETTING,
- IP_REPUTATION_LINKS_SETTING_DEFAULT,
- DEFAULT_INDEX_PATTERN,
-} from '../../../plugins/siem/common/constants';
+import { APP_ID, APP_NAME } from '../../../plugins/siem/common/constants';
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -63,101 +43,6 @@ export const siem = (kibana: any) => {
category: DEFAULT_APP_CATEGORIES.security,
},
],
- uiSettingDefaults: {
- [DEFAULT_SIEM_REFRESH_INTERVAL]: {
- type: 'json',
- name: i18n.translate('xpack.siem.uiSettings.defaultRefreshIntervalLabel', {
- defaultMessage: 'Time filter refresh interval',
- }),
- value: `{
- "pause": ${DEFAULT_INTERVAL_PAUSE},
- "value": ${DEFAULT_INTERVAL_VALUE}
-}`,
- description: i18n.translate('xpack.siem.uiSettings.defaultRefreshIntervalDescription', {
- defaultMessage:
- '
Default refresh interval for the SIEM time filter, in milliseconds.
',
- }),
- category: ['siem'],
- requiresPageReload: true,
- },
- [DEFAULT_SIEM_TIME_RANGE]: {
- type: 'json',
- name: i18n.translate('xpack.siem.uiSettings.defaultTimeRangeLabel', {
- defaultMessage: 'Time filter period',
- }),
- value: `{
- "from": "${DEFAULT_FROM}",
- "to": "${DEFAULT_TO}"
-}`,
- description: i18n.translate('xpack.siem.uiSettings.defaultTimeRangeDescription', {
- defaultMessage: 'Default period of time in the SIEM time filter.
',
- }),
- category: ['siem'],
- requiresPageReload: true,
- },
- [DEFAULT_INDEX_KEY]: {
- name: i18n.translate('xpack.siem.uiSettings.defaultIndexLabel', {
- defaultMessage: 'Elasticsearch indices',
- }),
- value: DEFAULT_INDEX_PATTERN,
- description: i18n.translate('xpack.siem.uiSettings.defaultIndexDescription', {
- defaultMessage:
- 'Comma-delimited list of Elasticsearch indices from which the SIEM app collects events.
',
- }),
- category: ['siem'],
- requiresPageReload: true,
- },
- [DEFAULT_ANOMALY_SCORE]: {
- name: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreLabel', {
- defaultMessage: 'Anomaly threshold',
- }),
- value: 50,
- type: 'number',
- description: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreDescription', {
- defaultMessage:
- 'Value above which Machine Learning job anomalies are displayed in the SIEM app.
Valid values: 0 to 100.
',
- }),
- category: ['siem'],
- requiresPageReload: true,
- },
- [ENABLE_NEWS_FEED_SETTING]: {
- name: i18n.translate('xpack.siem.uiSettings.enableNewsFeedLabel', {
- defaultMessage: 'News feed',
- }),
- value: true,
- description: i18n.translate('xpack.siem.uiSettings.enableNewsFeedDescription', {
- defaultMessage: 'Enables the News feed
',
- }),
- type: 'boolean',
- category: ['siem'],
- requiresPageReload: true,
- },
- [NEWS_FEED_URL_SETTING]: {
- name: i18n.translate('xpack.siem.uiSettings.newsFeedUrl', {
- defaultMessage: 'News feed URL',
- }),
- value: NEWS_FEED_URL_SETTING_DEFAULT,
- description: i18n.translate('xpack.siem.uiSettings.newsFeedUrlDescription', {
- defaultMessage: 'News feed content will be retrieved from this URL
',
- }),
- category: ['siem'],
- requiresPageReload: true,
- },
- [IP_REPUTATION_LINKS_SETTING]: {
- name: i18n.translate('xpack.siem.uiSettings.ipReputationLinks', {
- defaultMessage: 'IP Reputation Links',
- }),
- value: IP_REPUTATION_LINKS_SETTING_DEFAULT,
- type: 'json',
- description: i18n.translate('xpack.siem.uiSettings.ipReputationLinksDescription', {
- defaultMessage:
- 'Array of URL templates to build the list of reputation URLs to be displayed on the IP Details page.',
- }),
- category: ['siem'],
- requiresPageReload: true,
- },
- },
- mappings: savedObjectMappings,
},
config(Joi: Root) {
return Joi.object()
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts
index 9d2ac29bc47d7d..cc01edcfaab112 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts
@@ -3,6 +3,10 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks';
+jest.mock(
+ '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'
+);
export const mockFormHook = {
isSubmitted: false,
isSubmitting: false,
@@ -35,3 +39,5 @@ export const getFormMock = (sampleData: any) => ({
}),
getFormData: () => sampleData,
});
+
+export const useFormMock = useForm as jest.Mock;
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx
index d480744fc932a7..0897be6310fa2e 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx
@@ -14,6 +14,8 @@ import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router
import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline';
import { usePostCase } from '../../../../containers/case/use_post_case';
+import { useGetTags } from '../../../../containers/case/use_get_tags';
+
jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline');
jest.mock('../../../../containers/case/use_post_case');
import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks';
@@ -22,6 +24,14 @@ import { SiemPageName } from '../../../home/types';
jest.mock(
'../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'
);
+jest.mock('../../../../containers/case/use_get_tags');
+jest.mock(
+ '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider',
+ () => ({
+ FormDataProvider: ({ children }: { children: ({ tags }: { tags: string[] }) => void }) =>
+ children({ tags: ['rad', 'dude'] }),
+ })
+);
export const useFormMock = useForm as jest.Mock;
@@ -40,9 +50,11 @@ const defaultInsertTimeline = {
handleCursorChange,
handleOnTimelineChange,
};
+
+const sampleTags = ['coke', 'pepsi'];
const sampleData = {
description: 'what a great description',
- tags: ['coke', 'pepsi'],
+ tags: sampleTags,
title: 'what a cool title',
};
const defaultPostCase = {
@@ -52,14 +64,28 @@ const defaultPostCase = {
postCase,
};
describe('Create case', () => {
+ // Suppress warnings about "noSuggestions" prop
+ /* eslint-disable no-console */
+ const originalError = console.error;
+ beforeAll(() => {
+ console.error = jest.fn();
+ });
+ afterAll(() => {
+ console.error = originalError;
+ });
+ /* eslint-enable no-console */
+ const fetchTags = jest.fn();
const formHookMock = getFormMock(sampleData);
-
beforeEach(() => {
jest.resetAllMocks();
useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline);
usePostCaseMock.mockImplementation(() => defaultPostCase);
useFormMock.mockImplementation(() => ({ form: formHookMock }));
jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation);
+ (useGetTags as jest.Mock).mockImplementation(() => ({
+ tags: sampleTags,
+ fetchTags,
+ }));
});
it('should post case on submit click', async () => {
@@ -118,4 +144,19 @@ describe('Create case', () => {
);
expect(wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()).toBeTruthy();
});
+ it('Tag options render with new tags added', () => {
+ const wrapper = mount(
+
+
+
+
+
+ );
+ expect(
+ wrapper
+ .find(`[data-test-subj="caseTags"] [data-test-subj="input"]`)
+ .first()
+ .prop('options')
+ ).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]);
+ });
});
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx
index 53b792bb9b5ebb..0f819f961b3963 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/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, { useCallback, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
@@ -15,8 +15,16 @@ import {
import styled, { css } from 'styled-components';
import { Redirect } from 'react-router-dom';
+import { isEqual } from 'lodash/fp';
import { CasePostRequest } from '../../../../../../../../plugins/case/common/api';
-import { Field, Form, getUseField, useForm, UseField } from '../../../../shared_imports';
+import {
+ Field,
+ Form,
+ getUseField,
+ useForm,
+ UseField,
+ FormDataProvider,
+} from '../../../../shared_imports';
import { usePostCase } from '../../../../containers/case/use_post_case';
import { schema } from './schema';
import { InsertTimelinePopover } from '../../../../components/timeline/insert_timeline_popover';
@@ -24,6 +32,7 @@ import { useInsertTimeline } from '../../../../components/timeline/insert_timeli
import * as i18n from '../../translations';
import { SiemPageName } from '../../../home/types';
import { MarkdownEditorForm } from '../../../../components/markdown_editor/form';
+import { useGetTags } from '../../../../containers/case/use_get_tags';
export const CommonUseField = getUseField({ component: Field });
@@ -59,6 +68,21 @@ export const Create = React.memo(() => {
options: { stripEmptyFields: false },
schema,
});
+ const { tags: tagOptions } = useGetTags();
+ const [options, setOptions] = useState(
+ tagOptions.map(label => ({
+ label,
+ }))
+ );
+ useEffect(
+ () =>
+ setOptions(
+ tagOptions.map(label => ({
+ label,
+ }))
+ ),
+ [tagOptions]
+ );
const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline(
form,
'description'
@@ -108,6 +132,8 @@ export const Create = React.memo(() => {
fullWidth: true,
placeholder: '',
disabled: isLoading,
+ options,
+ noSuggestions: false,
},
}}
/>
@@ -131,6 +157,25 @@ export const Create = React.memo(() => {
}}
/>
+
+ {({ tags: anotherTags }) => {
+ const current: string[] = options.map(opt => opt.label);
+ const newOptions = anotherTags.reduce((acc: string[], item: string) => {
+ if (!acc.includes(item)) {
+ return [...acc, item];
+ }
+ return acc;
+ }, current);
+ if (!isEqual(current, newOptions)) {
+ setOptions(
+ newOptions.map((label: string) => ({
+ label,
+ }))
+ );
+ }
+ return null;
+ }}
+
({
+ FormDataProvider: ({ children }: { children: ({ tags }: { tags: string[] }) => void }) =>
+ children({ tags: ['rad', 'dude'] }),
+ })
+);
const onSubmit = jest.fn();
const defaultProps = {
disabled: false,
@@ -26,11 +35,27 @@ const defaultProps = {
};
describe('TagList ', () => {
+ // Suppress warnings about "noSuggestions" prop
+ /* eslint-disable no-console */
+ const originalError = console.error;
+ beforeAll(() => {
+ console.error = jest.fn();
+ });
+ afterAll(() => {
+ console.error = originalError;
+ });
+ /* eslint-enable no-console */
const sampleTags = ['coke', 'pepsi'];
+ const fetchTags = jest.fn();
const formHookMock = getFormMock({ tags: sampleTags });
beforeEach(() => {
jest.resetAllMocks();
(useForm as jest.Mock).mockImplementation(() => ({ form: formHookMock }));
+
+ (useGetTags as jest.Mock).mockImplementation(() => ({
+ tags: sampleTags,
+ fetchTags,
+ }));
});
it('Renders no tags, and then edit', () => {
const wrapper = mount(
@@ -80,6 +105,23 @@ describe('TagList ', () => {
expect(onSubmit).toBeCalledWith(sampleTags);
});
});
+ it('Tag options render with new tags added', () => {
+ const wrapper = mount(
+
+
+
+ );
+ wrapper
+ .find(`[data-test-subj="tag-list-edit-button"]`)
+ .last()
+ .simulate('click');
+ expect(
+ wrapper
+ .find(`[data-test-subj="caseTags"] [data-test-subj="input"]`)
+ .first()
+ .prop('options')
+ ).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]);
+ });
it('Cancels on cancel', async () => {
const props = {
...defaultProps,
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx
index 9bac000b93235d..c96ae09706426c 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useCallback, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import {
EuiText,
EuiHorizontalRule,
@@ -17,10 +17,12 @@ import {
EuiLoadingSpinner,
} from '@elastic/eui';
import styled, { css } from 'styled-components';
+import { isEqual } from 'lodash/fp';
import * as i18n from './translations';
-import { Form, useForm } from '../../../../shared_imports';
+import { Form, FormDataProvider, useForm } from '../../../../shared_imports';
import { schema } from './schema';
import { CommonUseField } from '../create';
+import { useGetTags } from '../../../../containers/case/use_get_tags';
interface TagListProps {
disabled?: boolean;
@@ -54,6 +56,22 @@ export const TagList = React.memo(
setIsEditTags(false);
}
}, [form, onSubmit]);
+ const { tags: tagOptions } = useGetTags();
+ const [options, setOptions] = useState(
+ tagOptions.map(label => ({
+ label,
+ }))
+ );
+
+ useEffect(
+ () =>
+ setOptions(
+ tagOptions.map(label => ({
+ label,
+ }))
+ ),
+ [tagOptions]
+ );
return (
@@ -75,7 +93,7 @@ export const TagList = React.memo(
)}
-
+
{tags.length === 0 && !isEditTags && {i18n.NO_TAGS}
}
{tags.length > 0 &&
!isEditTags &&
@@ -98,9 +116,30 @@ export const TagList = React.memo(
euiFieldProps: {
fullWidth: true,
placeholder: '',
+ options,
+ noSuggestions: false,
},
}}
/>
+
+ {({ tags: anotherTags }) => {
+ const current: string[] = options.map(opt => opt.label);
+ const newOptions = anotherTags.reduce((acc: string[], item: string) => {
+ if (!acc.includes(item)) {
+ return [...acc, item];
+ }
+ return acc;
+ }, current);
+ if (!isEqual(current, newOptions)) {
+ setOptions(
+ newOptions.map((label: string) => ({
+ label,
+ }))
+ );
+ }
+ return null;
+ }}
+
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx
index 1c71260422d4b1..ff402e8ea1c8b4 100644
--- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx
@@ -8,17 +8,13 @@ import React from 'react';
import { mount } from 'enzyme';
import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router';
-import { getFormMock } from '../__mock__/form';
+import { getFormMock, useFormMock } from '../__mock__/form';
import { useUpdateComment } from '../../../../containers/case/use_update_comment';
import { basicCase, getUserAction } from '../../../../containers/case/mock';
import { UserActionTree } from './';
import { TestProviders } from '../../../../mock';
-import { useFormMock } from '../create/index.test';
import { wait } from '../../../../lib/helpers';
import { act } from 'react-dom/test-utils';
-jest.mock(
- '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'
-);
const fetchUserActions = jest.fn();
const onUpdateField = jest.fn();
diff --git a/x-pack/legacy/plugins/siem/public/shared_imports.ts b/x-pack/legacy/plugins/siem/public/shared_imports.ts
index c83433ef129c97..0c0ac637a42293 100644
--- a/x-pack/legacy/plugins/siem/public/shared_imports.ts
+++ b/x-pack/legacy/plugins/siem/public/shared_imports.ts
@@ -8,6 +8,7 @@ export {
getUseField,
getFieldValidityAndErrorMessage,
FieldHook,
+ FieldValidateResponse,
FIELD_TYPES,
Form,
FormData,
@@ -17,6 +18,7 @@ export {
UseField,
useForm,
ValidationFunc,
+ VALIDATION_TYPES,
} from '../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
export {
Field,
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx
index 6c294d9c865488..7475229853698b 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx
@@ -4,12 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { MouseEvent, useMemo } from 'react';
+import React, { memo, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiTabs, EuiTab } from '@elastic/eui';
-import { useHistory, useLocation } from 'react-router-dom';
+import { useLocation } from 'react-router-dom';
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { Immutable } from '../../../../../common/types';
+import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler';
interface NavTabs {
name: string;
@@ -48,33 +49,30 @@ const navTabs: Immutable = [
},
];
-export const HeaderNavigation: React.FunctionComponent = React.memo(() => {
- const history = useHistory();
- const location = useLocation();
+const NavTab = memo<{ tab: NavTabs }>(({ tab }) => {
+ const { pathname } = useLocation();
const { services } = useKibana();
+ const onClickHandler = useNavigateByRouterEventHandler(tab.href);
const BASE_PATH = services.application.getUrlForApp('endpoint');
+ return (
+
+ {tab.name}
+
+ );
+});
+
+export const HeaderNavigation: React.FunctionComponent = React.memo(() => {
const tabList = useMemo(() => {
return navTabs.map((tab, index) => {
- return (
- {
- event.preventDefault();
- history.push(tab.href);
- }}
- isSelected={
- tab.href === location.pathname ||
- (tab.href !== '/' && location.pathname.startsWith(tab.href))
- }
- >
- {tab.name}
-
- );
+ return ;
});
- }, [BASE_PATH, history, location.pathname]);
+ }, []);
return {tabList};
});
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx
index d0a8f9690dafbf..2d4d1ca8a1b5b0 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx
@@ -110,7 +110,7 @@ describe('LinkToApp component', () => {
const clickEventArg = spyOnClickHandler.mock.calls[0][0];
expect(clickEventArg.isDefaultPrevented()).toBe(true);
});
- it('should not navigate if onClick callback prevents defalut', () => {
+ it('should not navigate if onClick callback prevents default', () => {
const spyOnClickHandler: LinkToAppOnClickMock = jest.fn(ev => {
ev.preventDefault();
});
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.test.tsx
new file mode 100644
index 00000000000000..b1f09617f01744
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.test.tsx
@@ -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 React from 'react';
+import { AppContextTestRender, createAppRootMockRenderer } from '../../mocks';
+import { useNavigateByRouterEventHandler } from './use_navigate_by_router_event_handler';
+import { act, fireEvent, cleanup } from '@testing-library/react';
+
+type ClickHandlerMock = jest.Mock<
+ Return,
+ [React.MouseEvent]
+>;
+
+describe('useNavigateByRouterEventHandler hook', () => {
+ let render: AppContextTestRender['render'];
+ let history: AppContextTestRender['history'];
+ let renderResult: ReturnType;
+ let linkEle: HTMLAnchorElement;
+ let clickHandlerSpy: ClickHandlerMock;
+ const Link = React.memo<{
+ routeTo: Parameters[0];
+ onClick?: Parameters[1];
+ }>(({ routeTo, onClick }) => {
+ const onClickHandler = useNavigateByRouterEventHandler(routeTo, onClick);
+ return (
+
+ mock link
+
+ );
+ });
+
+ beforeEach(async () => {
+ ({ render, history } = createAppRootMockRenderer());
+ clickHandlerSpy = jest.fn();
+ renderResult = render();
+ linkEle = (await renderResult.findByText('mock link')) as HTMLAnchorElement;
+ });
+ afterEach(cleanup);
+
+ it('should navigate to path via Router', () => {
+ const containerClickSpy = jest.fn();
+ renderResult.container.addEventListener('click', containerClickSpy);
+ expect(history.location.pathname).not.toEqual('/mock/path');
+ act(() => {
+ fireEvent.click(linkEle);
+ });
+ expect(containerClickSpy.mock.calls[0][0].defaultPrevented).toBe(true);
+ expect(history.location.pathname).toEqual('/mock/path');
+ renderResult.container.removeEventListener('click', containerClickSpy);
+ });
+ it('should support onClick prop', () => {
+ act(() => {
+ fireEvent.click(linkEle);
+ });
+ expect(clickHandlerSpy).toHaveBeenCalled();
+ expect(history.location.pathname).toEqual('/mock/path');
+ });
+ it('should not navigate if preventDefault is true', () => {
+ clickHandlerSpy.mockImplementation(event => {
+ event.preventDefault();
+ });
+ act(() => {
+ fireEvent.click(linkEle);
+ });
+ expect(history.location.pathname).not.toEqual('/mock/path');
+ });
+ it('should not navigate via router if click was not the primary mouse button', async () => {
+ act(() => {
+ fireEvent.click(linkEle, { button: 2 });
+ });
+ expect(history.location.pathname).not.toEqual('/mock/path');
+ });
+ it('should not navigate via router if anchor has target', () => {
+ linkEle.setAttribute('target', '_top');
+ act(() => {
+ fireEvent.click(linkEle, { button: 2 });
+ });
+ expect(history.location.pathname).not.toEqual('/mock/path');
+ });
+ it('should not to navigate if meta|alt|ctrl|shift keys are pressed', () => {
+ ['meta', 'alt', 'ctrl', 'shift'].forEach(key => {
+ act(() => {
+ fireEvent.click(linkEle, { [`${key}Key`]: true });
+ });
+ expect(history.location.pathname).not.toEqual('/mock/path');
+ });
+ });
+});
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.ts
new file mode 100644
index 00000000000000..dc33f0befaf357
--- /dev/null
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.ts
@@ -0,0 +1,70 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { MouseEventHandler, useCallback } from 'react';
+import { useHistory } from 'react-router-dom';
+import { LocationDescriptorObject } from 'history';
+
+type EventHandlerCallback = MouseEventHandler;
+
+/**
+ * Provides an event handler that can be used with (for example) `onClick` props to prevent the
+ * event's default behaviour and instead navigate to to a route via the Router
+ *
+ * @param routeTo
+ * @param onClick
+ */
+export const useNavigateByRouterEventHandler = (
+ routeTo: string | [string, unknown] | LocationDescriptorObject, // Cover the calling signature of `history.push()`
+
+ /** Additional onClick callback */
+ onClick?: EventHandlerCallback
+): EventHandlerCallback => {
+ const history = useHistory();
+ return useCallback(
+ ev => {
+ try {
+ if (onClick) {
+ onClick(ev);
+ }
+ } catch (error) {
+ ev.preventDefault();
+ throw error;
+ }
+
+ if (ev.defaultPrevented) {
+ return;
+ }
+
+ if (ev.button !== 0) {
+ return;
+ }
+
+ if (
+ ev.currentTarget instanceof HTMLAnchorElement &&
+ ev.currentTarget.target !== '' &&
+ ev.currentTarget.target !== '_self'
+ ) {
+ return;
+ }
+
+ if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) {
+ return;
+ }
+
+ ev.preventDefault();
+
+ if (Array.isArray(routeTo)) {
+ history.push(...routeTo);
+ } else if (typeof routeTo === 'string') {
+ history.push(routeTo);
+ } else {
+ history.push(routeTo);
+ }
+ },
+ [history, onClick, routeTo]
+ );
+};
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx
index 26f2203790a9e2..02f91307c988ec 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { memo } from 'react';
+import React, { memo, MouseEventHandler } from 'react';
import { EuiFlyoutHeader, CommonProps, EuiButtonEmpty } from '@elastic/eui';
import styled from 'styled-components';
@@ -12,7 +12,7 @@ export type FlyoutSubHeaderProps = CommonProps & {
children: React.ReactNode;
backButton?: {
title: string;
- onClick: (event: React.MouseEvent) => void;
+ onClick: MouseEventHandler;
href?: string;
};
};
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx
index 32c69426b03f33..336308b2ee2716 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx
@@ -16,13 +16,13 @@ import {
import React, { memo, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
-import { useHistory } from 'react-router-dom';
import { HostMetadata } from '../../../../../../common/types';
import { FormattedDateAndTime } from '../../formatted_date_time';
import { LinkToApp } from '../../components/link_to_app';
import { useHostListSelector, useHostLogsUrl } from '../hooks';
import { urlFromQueryParams } from '../url_from_query_params';
import { uiQueryParams } from '../../../store/hosts/selectors';
+import { useNavigateByRouterEventHandler } from '../../hooks/use_navigate_by_router_event_handler';
const HostIds = styled(EuiListGroupItem)`
margin-top: 0;
@@ -34,7 +34,6 @@ const HostIds = styled(EuiListGroupItem)`
export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
const { appId, appPath, url } = useHostLogsUrl(details.host.id);
const queryParams = useHostListSelector(uiQueryParams);
- const history = useHistory();
const detailsResultsUpper = useMemo(() => {
return [
{
@@ -65,6 +64,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
show: 'policy_response',
});
}, [details.host.id, queryParams]);
+ const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseUri);
const detailsResultsLower = useMemo(() => {
return [
@@ -84,10 +84,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
{
- ev.preventDefault();
- history.push(policyResponseUri);
- }}
+ onClick={policyStatusClickHandler}
>
{
details.endpoint.policy.id,
details.host.hostname,
details.host.ip,
- history,
- policyResponseUri,
+ policyResponseUri.search,
+ policyStatusClickHandler,
]);
return (
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx
index a41d4a968f177d..0c43e188225082 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx
@@ -24,6 +24,7 @@ import { HostDetails } from './host_details';
import { PolicyResponse } from './policy_response';
import { HostMetadata } from '../../../../../../common/types';
import { FlyoutSubHeader, FlyoutSubHeaderProps } from './components/flyout_sub_header';
+import { useNavigateByRouterEventHandler } from '../../hooks/use_navigate_by_router_event_handler';
export const HostDetailsFlyout = memo(() => {
const history = useHistory();
@@ -92,24 +93,25 @@ export const HostDetailsFlyout = memo(() => {
const PolicyResponseFlyoutPanel = memo<{
hostMeta: HostMetadata;
}>(({ hostMeta }) => {
- const history = useHistory();
const { show, ...queryParams } = useHostListSelector(uiQueryParams);
+ const detailsUri = useMemo(
+ () =>
+ urlFromQueryParams({
+ ...queryParams,
+ selected_host: hostMeta.host.id,
+ }),
+ [hostMeta.host.id, queryParams]
+ );
+ const backToDetailsClickHandler = useNavigateByRouterEventHandler(detailsUri);
const backButtonProp = useMemo((): FlyoutSubHeaderProps['backButton'] => {
- const detailsUri = urlFromQueryParams({
- ...queryParams,
- selected_host: hostMeta.host.id,
- });
return {
title: i18n.translate('xpack.endpoint.host.policyResponse.backLinkTitle', {
defaultMessage: 'Endpoint Details',
}),
href: '?' + detailsUri.search,
- onClick: ev => {
- ev.preventDefault();
- history.push(detailsUri);
- },
+ onClick: backToDetailsClickHandler,
};
- }, [history, hostMeta.host.id, queryParams]);
+ }, [backToDetailsClickHandler, detailsUri.search]);
return (
<>
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx
index 1d81d6e8a16dbe..e662bafed64926 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx
@@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useMemo, useCallback } from 'react';
+import React, { useMemo, useCallback, memo } from 'react';
import { useDispatch } from 'react-redux';
-import { useHistory } from 'react-router-dom';
import {
EuiPage,
EuiPageBody,
@@ -31,11 +30,26 @@ import { useHostListSelector } from './hooks';
import { CreateStructuredSelector } from '../../types';
import { urlFromQueryParams } from './url_from_query_params';
import { HostMetadata, Immutable } from '../../../../../common/types';
+import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler';
+
+const HostLink = memo<{
+ name: string;
+ href: string;
+ route: ReturnType;
+}>(({ name, href, route }) => {
+ const clickHandler = useNavigateByRouterEventHandler(route);
+
+ return (
+ // eslint-disable-next-line @elastic/eui/href-or-on-click
+
+ {name}
+
+ );
+});
const selector = (createStructuredSelector as CreateStructuredSelector)(selectors);
export const HostList = () => {
const dispatch = useDispatch<(a: HostAction) => void>();
- const history = useHistory();
const {
listData,
pageIndex,
@@ -75,18 +89,9 @@ export const HostList = () => {
defaultMessage: 'Hostname',
}),
render: ({ host: { hostname, id } }: { host: { hostname: string; id: string } }) => {
+ const newQueryParams = urlFromQueryParams({ ...queryParams, selected_host: id });
return (
- // eslint-disable-next-line @elastic/eui/href-or-on-click
- {
- ev.preventDefault();
- history.push(urlFromQueryParams({ ...queryParams, selected_host: id }));
- }}
- >
- {hostname}
-
+
);
},
},
@@ -150,7 +155,7 @@ export const HostList = () => {
},
},
];
- }, [queryParams, history]);
+ }, [queryParams]);
return (
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx
index 2ecc2b117bf017..d780b7bde8af34 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx
@@ -101,7 +101,7 @@ describe('Policy Details', () => {
'EuiPageHeaderSection[data-test-subj="pageViewHeaderLeft"] EuiButtonEmpty'
);
expect(history.location.pathname).toEqual('/policy/1');
- backToListButton.simulate('click');
+ backToListButton.simulate('click', { button: 0 });
expect(history.location.pathname).toEqual('/policy');
});
it('should display agent stats', async () => {
@@ -130,7 +130,7 @@ describe('Policy Details', () => {
'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]'
);
expect(history.location.pathname).toEqual('/policy/1');
- cancelbutton.simulate('click');
+ cancelbutton.simulate('click', { button: 0 });
expect(history.location.pathname).toEqual('/policy');
});
it('should display save button', async () => {
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx
index 076de7b57b44b4..ea9eb292dba1a9 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx
@@ -20,7 +20,6 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { useDispatch } from 'react-redux';
-import { useHistory } from 'react-router-dom';
import { usePolicyDetailsSelector } from './policy_hooks';
import {
policyDetails,
@@ -36,11 +35,11 @@ import { AgentsSummary } from './agents_summary';
import { VerticalDivider } from './vertical_divider';
import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events';
import { MalwareProtections } from './policy_forms/protections/malware';
+import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler';
export const PolicyDetails = React.memo(() => {
const dispatch = useDispatch<(action: AppAction) => void>();
const { notifications, services } = useKibana();
- const history = useHistory();
// Store values
const policyItem = usePolicyDetailsSelector(policyDetails);
@@ -82,13 +81,7 @@ export const PolicyDetails = React.memo(() => {
}
}, [notifications.toasts, policyItem, policyName, policyUpdateStatus]);
- const handleBackToListOnClick: React.MouseEventHandler = useCallback(
- ev => {
- ev.preventDefault();
- history.push(`/policy`);
- },
- [history]
- );
+ const handleBackToListOnClick = useNavigateByRouterEventHandler('/policy');
const handleSaveOnClick = useCallback(() => {
setShowConfirm(true);
diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx
index 062c7afb6706dd..f7eafff137f519 100644
--- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx
+++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx
@@ -24,30 +24,26 @@ import { useKibana } from '../../../../../../../../src/plugins/kibana_react/publ
import { PageView } from '../components/page_view';
import { LinkToApp } from '../components/link_to_app';
import { Immutable, PolicyData } from '../../../../../common/types';
+import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler';
interface TableChangeCallbackArguments {
page: { index: number; size: number };
}
-const PolicyLink: React.FC<{ name: string; route: string }> = ({ name, route }) => {
- const history = useHistory();
-
+const PolicyLink: React.FC<{ name: string; route: string; href: string }> = ({
+ name,
+ route,
+ href,
+}) => {
+ const clickHandler = useNavigateByRouterEventHandler(route);
return (
- {
- event.preventDefault();
- history.push(route);
- }}
- >
+ // eslint-disable-next-line @elastic/eui/href-or-on-click
+
{name}
);
};
-const renderPolicyNameLink = (value: string, item: Immutable) => {
- return ;
-};
-
export const PolicyList = React.memo(() => {
const { services, notifications } = useKibana();
const history = useHistory();
@@ -95,7 +91,16 @@ export const PolicyList = React.memo(() => {
name: i18n.translate('xpack.endpoint.policyList.nameField', {
defaultMessage: 'Policy Name',
}),
- render: renderPolicyNameLink,
+ render: (value: string, item: Immutable) => {
+ const routeUri = `/policy/${item.id}`;
+ return (
+
+ );
+ },
truncateText: true,
},
{
diff --git a/x-pack/plugins/endpoint/server/mocks.ts b/x-pack/plugins/endpoint/server/mocks.ts
index 903aa19cd88431..3881840efe9df0 100644
--- a/x-pack/plugins/endpoint/server/mocks.ts
+++ b/x-pack/plugins/endpoint/server/mocks.ts
@@ -4,6 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { IngestManagerSetupContract } from '../../ingest_manager/server';
+import { AgentService } from '../../ingest_manager/common/types';
+
/**
* Creates a mock IndexPatternRetriever for use in tests.
*
@@ -28,6 +31,15 @@ export const createMockMetadataIndexPatternRetriever = () => {
return createMockIndexPatternRetriever(MetadataIndexPattern);
};
+/**
+ * Creates a mock AgentService
+ */
+export const createMockAgentService = (): jest.Mocked => {
+ return {
+ getAgentStatusById: jest.fn(),
+ };
+};
+
/**
* Creates a mock IndexPatternService for use in tests that need to interact with the Ingest Manager's
* ESIndexPatternService.
@@ -35,10 +47,13 @@ export const createMockMetadataIndexPatternRetriever = () => {
* @param indexPattern a string index pattern to return when called by a test
* @returns the same value as `indexPattern` parameter
*/
-export const createMockIndexPatternService = (indexPattern: string) => {
+export const createMockIngestManagerSetupContract = (
+ indexPattern: string
+): IngestManagerSetupContract => {
return {
esIndexPatternService: {
getESIndexPattern: jest.fn().mockResolvedValue(indexPattern),
},
+ agentService: createMockAgentService(),
};
};
diff --git a/x-pack/plugins/endpoint/server/plugin.test.ts b/x-pack/plugins/endpoint/server/plugin.test.ts
index 8d55e64f16dcfa..c380bc5c3e3d05 100644
--- a/x-pack/plugins/endpoint/server/plugin.test.ts
+++ b/x-pack/plugins/endpoint/server/plugin.test.ts
@@ -7,7 +7,7 @@
import { EndpointPlugin, EndpointPluginSetupDependencies } from './plugin';
import { coreMock } from '../../../../src/core/server/mocks';
import { PluginSetupContract } from '../../features/server';
-import { createMockIndexPatternService } from './mocks';
+import { createMockIngestManagerSetupContract } from './mocks';
describe('test endpoint plugin', () => {
let plugin: EndpointPlugin;
@@ -31,7 +31,7 @@ describe('test endpoint plugin', () => {
};
mockedEndpointPluginSetupDependencies = {
features: mockedPluginSetupContract,
- ingestManager: createMockIndexPatternService(''),
+ ingestManager: createMockIngestManagerSetupContract(''),
};
});
diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts
index 6a42014e911303..ce6be5aeaf6db4 100644
--- a/x-pack/plugins/endpoint/server/plugin.ts
+++ b/x-pack/plugins/endpoint/server/plugin.ts
@@ -70,6 +70,7 @@ export class EndpointPlugin
plugins.ingestManager.esIndexPatternService,
this.initializerContext.logger
),
+ agentService: plugins.ingestManager.agentService,
logFactory: this.initializerContext.logger,
config: (): Promise => {
return createConfig$(this.initializerContext)
diff --git a/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts b/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts
index 6be7b268982060..39fc2ba4c74bb2 100644
--- a/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts
+++ b/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts
@@ -12,7 +12,7 @@ import {
import { registerAlertRoutes } from './index';
import { EndpointConfigSchema } from '../../config';
import { alertingIndexGetQuerySchema } from '../../../common/schema/alert_index';
-import { createMockIndexPatternRetriever } from '../../mocks';
+import { createMockAgentService, createMockIndexPatternRetriever } from '../../mocks';
describe('test alerts route', () => {
let routerMock: jest.Mocked;
@@ -26,6 +26,7 @@ describe('test alerts route', () => {
routerMock = httpServiceMock.createRouter();
registerAlertRoutes(routerMock, {
indexPatternRetriever: createMockIndexPatternRetriever('events-endpoint-*'),
+ agentService: createMockAgentService(),
logFactory: loggingServiceMock.create(),
config: () => Promise.resolve(EndpointConfigSchema.validate({})),
});
diff --git a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts
index 86e9f55da56970..9055ee4110fbba 100644
--- a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts
+++ b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts
@@ -37,7 +37,13 @@ export const alertDetailsHandlerWrapper = function(
indexPattern
);
- const currentHostInfo = await getHostData(ctx, response._source.host.id, indexPattern);
+ const currentHostInfo = await getHostData(
+ {
+ endpointAppContext,
+ requestHandlerContext: ctx,
+ },
+ response._source.host.id
+ );
return res.ok({
body: {
diff --git a/x-pack/plugins/endpoint/server/routes/metadata/index.ts b/x-pack/plugins/endpoint/server/routes/metadata/index.ts
index 883bb88204fd4f..bc79b828576e04 100644
--- a/x-pack/plugins/endpoint/server/routes/metadata/index.ts
+++ b/x-pack/plugins/endpoint/server/routes/metadata/index.ts
@@ -4,18 +4,29 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { IRouter, RequestHandlerContext } from 'kibana/server';
+import { IRouter, Logger, RequestHandlerContext } from 'kibana/server';
import { SearchResponse } from 'elasticsearch';
import { schema } from '@kbn/config-schema';
-import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders';
+import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders';
import { HostInfo, HostMetadata, HostResultList, HostStatus } from '../../../common/types';
import { EndpointAppContext } from '../../types';
+import { AgentStatus } from '../../../../ingest_manager/common/types/models';
interface HitSource {
_source: HostMetadata;
}
+interface MetadataRequestContext {
+ requestHandlerContext: RequestHandlerContext;
+ endpointAppContext: EndpointAppContext;
+}
+
+const HOST_STATUS_MAPPING = new Map([
+ ['online', HostStatus.ONLINE],
+ ['offline', HostStatus.OFFLINE],
+]);
+
export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) {
router.post(
{
@@ -62,7 +73,12 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp
'search',
queryParams
)) as SearchResponse;
- return res.ok({ body: mapToHostResultList(queryParams, response) });
+ return res.ok({
+ body: await mapToHostResultList(queryParams, response, {
+ endpointAppContext,
+ requestHandlerContext: context,
+ }),
+ });
} catch (err) {
return res.internalError({ body: err });
}
@@ -79,11 +95,13 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp
},
async (context, req, res) => {
try {
- const index = await endpointAppContext.indexPatternRetriever.getMetadataIndexPattern(
- context
+ const doc = await getHostData(
+ {
+ endpointAppContext,
+ requestHandlerContext: context,
+ },
+ req.params.id
);
-
- const doc = await getHostData(context, req.params.id, index);
if (doc) {
return res.ok({ body: doc });
}
@@ -96,12 +114,14 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp
}
export async function getHostData(
- context: RequestHandlerContext,
- id: string,
- index: string
+ metadataRequestContext: MetadataRequestContext,
+ id: string
): Promise {
+ const index = await metadataRequestContext.endpointAppContext.indexPatternRetriever.getMetadataIndexPattern(
+ metadataRequestContext.requestHandlerContext
+ );
const query = getESQueryHostMetadataByID(id, index);
- const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser(
+ const response = (await metadataRequestContext.requestHandlerContext.core.elasticsearch.dataClient.callAsCurrentUser(
'search',
query
)) as SearchResponse;
@@ -110,22 +130,25 @@ export async function getHostData(
return undefined;
}
- return enrichHostMetadata(response.hits.hits[0]._source);
+ return await enrichHostMetadata(response.hits.hits[0]._source, metadataRequestContext);
}
-function mapToHostResultList(
+async function mapToHostResultList(
queryParams: Record,
- searchResponse: SearchResponse
-): HostResultList {
+ searchResponse: SearchResponse,
+ metadataRequestContext: MetadataRequestContext
+): Promise {
const totalNumberOfHosts = searchResponse?.aggregations?.total?.value || 0;
if (searchResponse.hits.hits.length > 0) {
return {
request_page_size: queryParams.size,
request_page_index: queryParams.from,
- hosts: searchResponse.hits.hits
- .map(response => response.inner_hits.most_recent.hits.hits)
- .flatMap(data => data as HitSource)
- .map(entry => enrichHostMetadata(entry._source)),
+ hosts: await Promise.all(
+ searchResponse.hits.hits
+ .map(response => response.inner_hits.most_recent.hits.hits)
+ .flatMap(data => data as HitSource)
+ .map(async entry => enrichHostMetadata(entry._source, metadataRequestContext))
+ ),
total: totalNumberOfHosts,
};
} else {
@@ -138,9 +161,43 @@ function mapToHostResultList(
}
}
-function enrichHostMetadata(hostMetadata: HostMetadata): HostInfo {
+async function enrichHostMetadata(
+ hostMetadata: HostMetadata,
+ metadataRequestContext: MetadataRequestContext
+): Promise {
+ let hostStatus = HostStatus.ERROR;
+ let elasticAgentId = hostMetadata?.elastic?.agent?.id;
+ const log = logger(metadataRequestContext.endpointAppContext);
+ try {
+ /**
+ * Get agent status by elastic agent id if available or use the host id.
+ * https://github.com/elastic/endpoint-app-team/issues/354
+ */
+
+ if (!elasticAgentId) {
+ elasticAgentId = hostMetadata.host.id;
+ log.warn(`Missing elastic agent id, using host id instead ${elasticAgentId}`);
+ }
+
+ const status = await metadataRequestContext.endpointAppContext.agentService.getAgentStatusById(
+ metadataRequestContext.requestHandlerContext.core.savedObjects.client,
+ elasticAgentId
+ );
+ hostStatus = HOST_STATUS_MAPPING.get(status) || HostStatus.ERROR;
+ } catch (e) {
+ if (e.isBoom && e.output.statusCode === 404) {
+ log.warn(`agent with id ${elasticAgentId} not found`);
+ } else {
+ log.error(e);
+ throw e;
+ }
+ }
return {
metadata: hostMetadata,
- host_status: HostStatus.ERROR,
+ host_status: hostStatus,
};
}
+
+const logger = (endpointAppContext: EndpointAppContext): Logger => {
+ return endpointAppContext.logFactory.get('metadata');
+};
diff --git a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts
index 9a7d3fb3188a64..a1186aabc7a66a 100644
--- a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts
+++ b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts
@@ -25,7 +25,9 @@ import { SearchResponse } from 'elasticsearch';
import { registerEndpointRoutes } from './index';
import { EndpointConfigSchema } from '../../config';
import * as data from '../../test_data/all_metadata_data.json';
-import { createMockMetadataIndexPatternRetriever } from '../../mocks';
+import { createMockAgentService, createMockMetadataIndexPatternRetriever } from '../../mocks';
+import { AgentService } from '../../../../ingest_manager/common/types';
+import Boom from 'boom';
describe('test endpoint route', () => {
let routerMock: jest.Mocked;
@@ -35,6 +37,7 @@ describe('test endpoint route', () => {
let mockSavedObjectClient: jest.Mocked;
let routeHandler: RequestHandler;
let routeConfig: RouteConfig;
+ let mockAgentService: jest.Mocked;
beforeEach(() => {
mockClusterClient = elasticsearchServiceMock.createClusterClient() as jest.Mocked<
@@ -45,8 +48,10 @@ describe('test endpoint route', () => {
mockClusterClient.asScoped.mockReturnValue(mockScopedClient);
routerMock = httpServiceMock.createRouter();
mockResponse = httpServerMock.createResponseFactory();
+ mockAgentService = createMockAgentService();
registerEndpointRoutes(routerMock, {
indexPatternRetriever: createMockMetadataIndexPatternRetriever(),
+ agentService: mockAgentService,
logFactory: loggingServiceMock.create(),
config: () => Promise.resolve(EndpointConfigSchema.validate({})),
});
@@ -83,7 +88,7 @@ describe('test endpoint route', () => {
[routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/metadata')
)!;
-
+ mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
@@ -113,6 +118,8 @@ describe('test endpoint route', () => {
],
},
});
+
+ mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() =>
Promise.resolve((data as unknown) as SearchResponse)
);
@@ -154,6 +161,8 @@ describe('test endpoint route', () => {
filter: 'not host.ip:10.140.73.246',
},
});
+
+ mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() =>
Promise.resolve((data as unknown) as SearchResponse)
);
@@ -216,10 +225,10 @@ describe('test endpoint route', () => {
},
})
);
+ mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error');
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/metadata')
)!;
-
await routeHandler(
createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
mockRequest,
@@ -233,13 +242,14 @@ describe('test endpoint route', () => {
expect(message).toEqual('Endpoint Not Found');
});
- it('should return a single endpoint with status error', async () => {
+ it('should return a single endpoint with status online', async () => {
const mockRequest = httpServerMock.createKibanaRequest({
params: { id: (data as any).hits.hits[0]._id },
});
const response: SearchResponse = (data as unknown) as SearchResponse<
HostMetadata
>;
+ mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('online');
mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
[routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
path.startsWith('/api/endpoint/metadata')
@@ -256,6 +266,64 @@ describe('test endpoint route', () => {
expect(mockResponse.ok).toBeCalled();
const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
expect(result).toHaveProperty('metadata.endpoint');
+ expect(result.host_status).toEqual(HostStatus.ONLINE);
+ });
+
+ it('should return a single endpoint with status error when AgentService throw 404', async () => {
+ const response: SearchResponse = (data as unknown) as SearchResponse<
+ HostMetadata
+ >;
+
+ const mockRequest = httpServerMock.createKibanaRequest({
+ params: { id: response.hits.hits[0]._id },
+ });
+
+ mockAgentService.getAgentStatusById = jest.fn().mockImplementation(() => {
+ throw Boom.notFound('Agent not found');
+ });
+ mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
+ [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
+ path.startsWith('/api/endpoint/metadata')
+ )!;
+
+ await routeHandler(
+ createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
+ mockRequest,
+ mockResponse
+ );
+
+ expect(mockScopedClient.callAsCurrentUser).toBeCalled();
+ expect(routeConfig.options).toEqual({ authRequired: true });
+ expect(mockResponse.ok).toBeCalled();
+ const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
+ expect(result.host_status).toEqual(HostStatus.ERROR);
+ });
+
+ it('should return a single endpoint with status error when status is not offline or online', async () => {
+ const response: SearchResponse = (data as unknown) as SearchResponse<
+ HostMetadata
+ >;
+
+ const mockRequest = httpServerMock.createKibanaRequest({
+ params: { id: response.hits.hits[0]._id },
+ });
+
+ mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('warning');
+ mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response));
+ [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) =>
+ path.startsWith('/api/endpoint/metadata')
+ )!;
+
+ await routeHandler(
+ createRouteHandlerContext(mockScopedClient, mockSavedObjectClient),
+ mockRequest,
+ mockResponse
+ );
+
+ expect(mockScopedClient.callAsCurrentUser).toBeCalled();
+ expect(routeConfig.options).toEqual({ authRequired: true });
+ expect(mockResponse.ok).toBeCalled();
+ const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
expect(result.host_status).toEqual(HostStatus.ERROR);
});
});
diff --git a/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts
index c8143fbdda1ea1..7e6e3f875cd4ca 100644
--- a/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts
+++ b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts
@@ -6,7 +6,11 @@
import { httpServerMock, loggingServiceMock } from '../../../../../../src/core/server/mocks';
import { EndpointConfigSchema } from '../../config';
import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders';
-import { createMockMetadataIndexPatternRetriever, MetadataIndexPattern } from '../../mocks';
+import {
+ createMockAgentService,
+ createMockMetadataIndexPatternRetriever,
+ MetadataIndexPattern,
+} from '../../mocks';
describe('query builder', () => {
describe('MetadataListESQuery', () => {
@@ -18,6 +22,7 @@ describe('query builder', () => {
mockRequest,
{
indexPatternRetriever: createMockMetadataIndexPatternRetriever(),
+ agentService: createMockAgentService(),
logFactory: loggingServiceMock.create(),
config: () => Promise.resolve(EndpointConfigSchema.validate({})),
},
@@ -69,6 +74,7 @@ describe('query builder', () => {
mockRequest,
{
indexPatternRetriever: createMockMetadataIndexPatternRetriever(),
+ agentService: createMockAgentService(),
logFactory: loggingServiceMock.create(),
config: () => Promise.resolve(EndpointConfigSchema.validate({})),
},
diff --git a/x-pack/plugins/endpoint/server/types.ts b/x-pack/plugins/endpoint/server/types.ts
index 46a23060339f41..d43ec58aec4282 100644
--- a/x-pack/plugins/endpoint/server/types.ts
+++ b/x-pack/plugins/endpoint/server/types.ts
@@ -6,12 +6,14 @@
import { LoggerFactory } from 'kibana/server';
import { EndpointConfigType } from './config';
import { IndexPatternRetriever } from './index_pattern';
+import { AgentService } from '../../ingest_manager/common/types';
/**
* The context for Endpoint apps.
*/
export interface EndpointAppContext {
indexPatternRetriever: IndexPatternRetriever;
+ agentService: AgentService;
logFactory: LoggerFactory;
config(): Promise;
}
diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts
index 42f7a9333118e5..150a4c9d602802 100644
--- a/x-pack/plugins/ingest_manager/common/types/index.ts
+++ b/x-pack/plugins/ingest_manager/common/types/index.ts
@@ -3,9 +3,24 @@
* 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 'kibana/server';
+import { AgentStatus } from './models';
+
export * from './models';
export * from './rest_spec';
+/**
+ * A service that provides exported functions that return information about an Agent
+ */
+export interface AgentService {
+ /**
+ * Return the status by the Agent's id
+ * @param soClient
+ * @param agentId
+ */
+ getAgentStatusById(soClient: SavedObjectsClientContract, agentId: string): Promise;
+}
+
export interface IngestManagerConfigType {
enabled: boolean;
epm: {
diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
index 53ad0310ea6134..a13e1655d56665 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
@@ -256,6 +256,10 @@ export enum DefaultPackages {
endpoint = 'endpoint',
}
+export interface IndexTemplateMappings {
+ properties: any;
+}
+
export interface IndexTemplate {
order: number;
index_patterns: string[];
@@ -263,3 +267,8 @@ export interface IndexTemplate {
mappings: object;
aliases: object;
}
+
+export interface TemplateRef {
+ templateName: string;
+ indexTemplate: IndexTemplate;
+}
diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts
index 9b8e1702f4f2f1..eeb9bb355392b8 100644
--- a/x-pack/plugins/ingest_manager/server/plugin.ts
+++ b/x-pack/plugins/ingest_manager/server/plugin.ts
@@ -40,18 +40,20 @@ import {
registerInstallScriptRoutes,
} from './routes';
-import { IngestManagerConfigType } from '../common';
+import { AgentService, IngestManagerConfigType } from '../common';
import {
appContextService,
ESIndexPatternService,
ESIndexPatternSavedObjectService,
} from './services';
+import { getAgentStatusById } from './services/agents';
/**
* Describes public IngestManager plugin contract returned at the `setup` stage.
*/
export interface IngestManagerSetupContract {
esIndexPatternService: ESIndexPatternService;
+ agentService: AgentService;
}
export interface IngestManagerSetupDeps {
@@ -150,6 +152,9 @@ export class IngestManagerPlugin implements Plugin {
}
return deepFreeze({
esIndexPatternService: new ESIndexPatternSavedObjectService(),
+ agentService: {
+ getAgentStatusById,
+ },
});
}
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts
new file mode 100644
index 00000000000000..d19fe883a7780e
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock';
+import { getAgentStatusById } from './status';
+import { AGENT_TYPE_PERMANENT } from '../../../common/constants';
+import { AgentSOAttributes } from '../../../common/types/models';
+import { SavedObject } from 'kibana/server';
+
+describe('Agent status service', () => {
+ it('should return inactive when agent is not active', async () => {
+ const mockSavedObjectsClient = savedObjectsClientMock.create();
+ mockSavedObjectsClient.get = jest.fn().mockReturnValue({
+ id: 'id',
+ type: AGENT_TYPE_PERMANENT,
+ attributes: {
+ active: false,
+ local_metadata: '{}',
+ user_provided_metadata: '{}',
+ },
+ } as SavedObject);
+ const status = await getAgentStatusById(mockSavedObjectsClient, 'id');
+ expect(status).toEqual('inactive');
+ });
+
+ it('should return online when agent is active', async () => {
+ const mockSavedObjectsClient = savedObjectsClientMock.create();
+ mockSavedObjectsClient.get = jest.fn().mockReturnValue({
+ id: 'id',
+ type: AGENT_TYPE_PERMANENT,
+ attributes: {
+ active: true,
+ local_metadata: '{}',
+ user_provided_metadata: '{}',
+ },
+ } as SavedObject);
+ const status = await getAgentStatusById(mockSavedObjectsClient, 'id');
+ expect(status).toEqual('online');
+ });
+});
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.ts
index 21e200d701e69d..001b6d01f078ee 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/status.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/status.ts
@@ -5,7 +5,7 @@
*/
import { SavedObjectsClientContract } from 'src/core/server';
-import { listAgents } from './crud';
+import { getAgent, listAgents } from './crud';
import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../constants';
import { AgentStatus, Agent } from '../../types';
@@ -17,6 +17,14 @@ import {
} from '../../constants';
import { AgentStatusKueryHelper } from '../../../common/services';
+export async function getAgentStatusById(
+ soClient: SavedObjectsClientContract,
+ agentId: string
+): Promise {
+ const agent = await getAgent(soClient, agentId);
+ return getAgentStatus(agent);
+}
+
export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentStatus {
const { type, last_checkin: lastCheckIn } = agent;
const msLastCheckIn = new Date(lastCheckIn || 0).getTime();
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
index 166983fbccc35a..5cf1f241a709fe 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
@@ -28,9 +28,6 @@ exports[`tests loading base.yml: base.yml 1`] = `
}
},
"mappings": {
- "_meta": {
- "package": "foo"
- },
"dynamic_templates": [
{
"strings_as_keyword": {
@@ -123,9 +120,6 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = `
}
},
"mappings": {
- "_meta": {
- "package": "foo"
- },
"dynamic_templates": [
{
"strings_as_keyword": {
@@ -218,9 +212,6 @@ exports[`tests loading system.yml: system.yml 1`] = `
}
},
"mappings": {
- "_meta": {
- "package": "foo"
- },
"dynamic_templates": [
{
"strings_as_keyword": {
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts
index 560ddfc1f68857..4df626259ece76 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts
@@ -4,13 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- AssetReference,
- Dataset,
- RegistryPackage,
- IngestAssetType,
- ElasticsearchAssetType,
-} from '../../../../types';
+import { Dataset, RegistryPackage, ElasticsearchAssetType, TemplateRef } from '../../../../types';
import { CallESAsCurrentUser } from '../../../../types';
import { Field, loadFieldsFromYaml, processFields } from '../../fields/field';
import { getPipelineNameForInstallation } from '../ingest_pipeline/install';
@@ -22,7 +16,7 @@ export const installTemplates = async (
callCluster: CallESAsCurrentUser,
pkgName: string,
pkgVersion: string
-) => {
+): Promise => {
// install any pre-built index template assets,
// atm, this is only the base package's global template
installPreBuiltTemplates(pkgName, pkgVersion, callCluster);
@@ -30,7 +24,7 @@ export const installTemplates = async (
// build templates per dataset from yml files
const datasets = registryPackage.datasets;
if (datasets) {
- const templates = datasets.reduce>>((acc, dataset) => {
+ const installTemplatePromises = datasets.reduce>>((acc, dataset) => {
acc.push(
installTemplateForDataset({
pkg: registryPackage,
@@ -40,7 +34,9 @@ export const installTemplates = async (
);
return acc;
}, []);
- return Promise.all(templates).then(results => results.flat());
+
+ const res = await Promise.all(installTemplatePromises);
+ return res.flat();
}
return [];
};
@@ -84,7 +80,7 @@ export async function installTemplateForDataset({
pkg: RegistryPackage;
callCluster: CallESAsCurrentUser;
dataset: Dataset;
-}): Promise {
+}): Promise {
const fields = await loadFieldsFromYaml(pkg, dataset.path);
return installTemplate({
callCluster,
@@ -104,7 +100,7 @@ export async function installTemplate({
fields: Field[];
dataset: Dataset;
packageVersion: string;
-}): Promise {
+}): Promise {
const mappings = generateMappings(processFields(fields));
const templateName = generateTemplateName(dataset);
let pipelineName;
@@ -122,6 +118,8 @@ export async function installTemplate({
body: template,
});
- // The id of a template is its name
- return { id: templateName, type: IngestAssetType.IndexTemplate };
+ return {
+ templateName,
+ indexTemplate: template,
+ };
}
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
index 22a61d2bdfb7c3..46b6923962462e 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
@@ -5,24 +5,30 @@
*/
import { Field, Fields } from '../../fields/field';
-import { Dataset, IndexTemplate } from '../../../../types';
+import {
+ Dataset,
+ CallESAsCurrentUser,
+ TemplateRef,
+ IndexTemplate,
+ IndexTemplateMappings,
+} from '../../../../types';
import { getDatasetAssetBaseName } from '../index';
interface Properties {
[key: string]: any;
}
-interface Mappings {
- properties: any;
-}
-
-interface Mapping {
- [key: string]: any;
-}
interface MultiFields {
[key: string]: object;
}
+export interface IndexTemplateMapping {
+ [key: string]: any;
+}
+export interface CurrentIndex {
+ indexName: string;
+ indexTemplate: IndexTemplate;
+}
const DEFAULT_SCALING_FACTOR = 1000;
const DEFAULT_IGNORE_ABOVE = 1024;
@@ -34,7 +40,7 @@ const DEFAULT_IGNORE_ABOVE = 1024;
export function getTemplate(
type: string,
templateName: string,
- mappings: Mappings,
+ mappings: IndexTemplateMappings,
pipelineName?: string | undefined
): IndexTemplate {
const template = getBaseTemplate(type, templateName, mappings);
@@ -52,7 +58,7 @@ export function getTemplate(
*
* @param fields
*/
-export function generateMappings(fields: Field[]): Mappings {
+export function generateMappings(fields: Field[]): IndexTemplateMappings {
const props: Properties = {};
// TODO: this can happen when the fields property in fields.yml is present but empty
// Maybe validation should be moved to fields/field.ts
@@ -140,8 +146,8 @@ function generateMultiFields(fields: Fields): MultiFields {
return multiFields;
}
-function generateKeywordMapping(field: Field): Mapping {
- const mapping: Mapping = {
+function generateKeywordMapping(field: Field): IndexTemplateMapping {
+ const mapping: IndexTemplateMapping = {
ignore_above: DEFAULT_IGNORE_ABOVE,
};
if (field.ignore_above) {
@@ -150,8 +156,8 @@ function generateKeywordMapping(field: Field): Mapping {
return mapping;
}
-function generateTextMapping(field: Field): Mapping {
- const mapping: Mapping = {};
+function generateTextMapping(field: Field): IndexTemplateMapping {
+ const mapping: IndexTemplateMapping = {};
if (field.analyzer) {
mapping.analyzer = field.analyzer;
}
@@ -200,7 +206,11 @@ export function generateESIndexPatterns(datasets: Dataset[] | undefined): Record
return patterns;
}
-function getBaseTemplate(type: string, templateName: string, mappings: Mappings): IndexTemplate {
+function getBaseTemplate(
+ type: string,
+ templateName: string,
+ mappings: IndexTemplateMappings
+): IndexTemplate {
return {
// We need to decide which order we use for the templates
order: 1,
@@ -234,10 +244,6 @@ function getBaseTemplate(type: string, templateName: string, mappings: Mappings)
},
},
mappings: {
- // To be filled with interesting information about this specific index
- _meta: {
- package: 'foo',
- },
// All the dynamic field mappings
dynamic_templates: [
// This makes sure all mappings are keywords by default
@@ -261,3 +267,112 @@ function getBaseTemplate(type: string, templateName: string, mappings: Mappings)
aliases: {},
};
}
+
+export const updateCurrentWriteIndices = async (
+ callCluster: CallESAsCurrentUser,
+ templates: TemplateRef[]
+): Promise => {
+ if (!templates) return;
+
+ const allIndices = await queryIndicesFromTemplates(callCluster, templates);
+ return updateAllIndices(allIndices, callCluster);
+};
+
+const queryIndicesFromTemplates = async (
+ callCluster: CallESAsCurrentUser,
+ templates: TemplateRef[]
+): Promise => {
+ const indexPromises = templates.map(template => {
+ return getIndices(callCluster, template);
+ });
+ const indexObjects = await Promise.all(indexPromises);
+ return indexObjects.filter(item => item !== undefined).flat();
+};
+
+const getIndices = async (
+ callCluster: CallESAsCurrentUser,
+ template: TemplateRef
+): Promise => {
+ const { templateName, indexTemplate } = template;
+ const res = await callCluster('search', getIndexQuery(templateName));
+ const indices: any[] = res?.aggregations?.index.buckets;
+ if (indices) {
+ return indices.map(index => ({
+ indexName: index.key,
+ indexTemplate,
+ }));
+ }
+};
+
+const updateAllIndices = async (
+ indexNameWithTemplates: CurrentIndex[],
+ callCluster: CallESAsCurrentUser
+): Promise => {
+ const updateIndexPromises = indexNameWithTemplates.map(({ indexName, indexTemplate }) => {
+ return updateExistingIndex({ indexName, callCluster, indexTemplate });
+ });
+ await Promise.all(updateIndexPromises);
+};
+const updateExistingIndex = async ({
+ indexName,
+ callCluster,
+ indexTemplate,
+}: {
+ indexName: string;
+ callCluster: CallESAsCurrentUser;
+ indexTemplate: IndexTemplate;
+}) => {
+ const { settings, mappings } = indexTemplate;
+ // try to update the mappings first
+ // for now we assume updates are compatible
+ try {
+ await callCluster('indices.putMapping', {
+ index: indexName,
+ body: mappings,
+ });
+ } catch (err) {
+ throw new Error('incompatible mappings update');
+ }
+ // update settings after mappings was successful to ensure
+ // pointing to theme new pipeline is safe
+ // for now, only update the pipeline
+ if (!settings.index.default_pipeline) return;
+ try {
+ await callCluster('indices.putSettings', {
+ index: indexName,
+ body: { index: { default_pipeline: settings.index.default_pipeline } },
+ });
+ } catch (err) {
+ throw new Error('incompatible settings update');
+ }
+};
+
+const getIndexQuery = (templateName: string) => ({
+ index: `${templateName}-*`,
+ size: 0,
+ body: {
+ query: {
+ bool: {
+ must: [
+ {
+ exists: {
+ field: 'stream.namespace',
+ },
+ },
+ {
+ exists: {
+ field: 'stream.dataset',
+ },
+ },
+ ],
+ },
+ },
+ aggs: {
+ index: {
+ terms: {
+ field: '_index',
+ },
+ },
+ },
+ },
+});
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 0a7642752b3e98..f3bd49eab6038b 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
@@ -12,15 +12,19 @@ import {
KibanaAssetType,
CallESAsCurrentUser,
DefaultPackages,
+ ElasticsearchAssetType,
+ IngestAssetType,
} from '../../../types';
import { installIndexPatterns } from '../kibana/index_pattern/install';
import * as Registry from '../registry';
import { getObject } from './get_objects';
-import { getInstallation } from './index';
+import { getInstallation, getInstallationObject } from './index';
import { installTemplates } from '../elasticsearch/template/install';
import { generateESIndexPatterns } from '../elasticsearch/template/template';
import { installPipelines } from '../elasticsearch/ingest_pipeline/install';
import { installILMPolicy } from '../elasticsearch/ilm/install';
+import { deleteAssetsByType, deleteKibanaSavedObjectsAssets } from './remove';
+import { updateCurrentWriteIndices } from '../elasticsearch/template/template';
export async function installLatestPackage(options: {
savedObjectsClient: SavedObjectsClientContract;
@@ -89,41 +93,80 @@ export async function installPackage(options: {
const { savedObjectsClient, pkgkey, callCluster } = options;
// TODO: change epm API to /packageName/version so we don't need to do this
const [pkgName, pkgVersion] = pkgkey.split('-');
+ // see if some version of this package is already installed
+ const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName });
+ const reinstall = pkgVersion === installedPkg?.attributes.version;
+
const registryPackageInfo = await Registry.fetchInfo(pkgName, pkgVersion);
const { internal = false } = registryPackageInfo;
- const installKibanaAssetsPromise = installKibanaAssets({
- savedObjectsClient,
- pkgName,
- pkgVersion,
- });
- const installPipelinePromises = installPipelines(registryPackageInfo, callCluster);
- const installTemplatePromises = installTemplates(
+ // delete the previous version's installation's SO kibana assets before installing new ones
+ // in case some assets were removed in the new version
+ if (installedPkg) {
+ try {
+ await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedPkg.attributes.installed);
+ } catch (err) {
+ // some assets may not exist if deleting during a failed update
+ }
+ }
+
+ const [installedKibanaAssets, installedPipelines] = await Promise.all([
+ installKibanaAssets({
+ savedObjectsClient,
+ pkgName,
+ pkgVersion,
+ }),
+ installPipelines(registryPackageInfo, callCluster),
+ // index patterns and ilm policies are not currently associated with a particular package
+ // so we do not save them in the package saved object state.
+ installIndexPatterns(savedObjectsClient, pkgName, pkgVersion),
+ // currenly only the base package has an ILM policy
+ // at some point ILM policies can be installed/modified
+ // per dataset and we should then save them
+ installILMPolicy(pkgName, pkgVersion, callCluster),
+ ]);
+
+ // install or update the templates
+ const installedTemplates = await installTemplates(
registryPackageInfo,
callCluster,
pkgName,
pkgVersion
);
+ const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets);
- // index patterns and ilm policies are not currently associated with a particular package
- // so we do not save them in the package saved object state. at some point ILM policies can be installed/modified
- // per dataset and we should then save them
- await installIndexPatterns(savedObjectsClient, pkgName, pkgVersion);
- // currenly only the base package has an ILM policy
- await installILMPolicy(pkgName, pkgVersion, callCluster);
-
- const res = await Promise.all([
- installKibanaAssetsPromise,
- installPipelinePromises,
- installTemplatePromises,
- ]);
+ // get template refs to save
+ const installedTemplateRefs = installedTemplates.map(template => ({
+ id: template.templateName,
+ type: IngestAssetType.IndexTemplate,
+ }));
- const toSaveAssetRefs: AssetReference[] = res.flat();
- const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets);
- // Save those references in the package manager's state saved object
- return await saveInstallationReferences({
+ if (installedPkg) {
+ // update current index for every index template created
+ await updateCurrentWriteIndices(callCluster, installedTemplates);
+ if (!reinstall) {
+ try {
+ // delete the previous version's installation's pipelines
+ // this must happen after the template is updated
+ await deleteAssetsByType({
+ savedObjectsClient,
+ callCluster,
+ installedObjects: installedPkg.attributes.installed,
+ assetType: ElasticsearchAssetType.ingestPipeline,
+ });
+ } catch (err) {
+ throw new Error(err.message);
+ }
+ }
+ }
+ const toSaveAssetRefs: AssetReference[] = [
+ ...installedKibanaAssets,
+ ...installedPipelines,
+ ...installedTemplateRefs,
+ ];
+ // Save references to installed assets in the package's saved object state
+ return saveInstallationReferences({
savedObjectsClient,
- pkgkey,
pkgName,
pkgVersion,
internal,
@@ -154,7 +197,6 @@ export async function installKibanaAssets(options: {
export async function saveInstallationReferences(options: {
savedObjectsClient: SavedObjectsClientContract;
- pkgkey: string;
pkgName: string;
pkgVersion: string;
internal: boolean;
@@ -169,25 +211,12 @@ export async function saveInstallationReferences(options: {
toSaveAssetRefs,
toSaveESIndexPatterns,
} = options;
- const installation = await getInstallation({ savedObjectsClient, pkgName });
- const savedAssetRefs = installation?.installed || [];
- const toInstallESIndexPatterns = Object.assign(
- installation?.es_index_patterns || {},
- toSaveESIndexPatterns
- );
-
- const mergeRefsReducer = (current: AssetReference[], pending: AssetReference) => {
- const hasRef = current.find(c => c.id === pending.id && c.type === pending.type);
- if (!hasRef) current.push(pending);
- return current;
- };
- const toInstallAssetsRefs = toSaveAssetRefs.reduce(mergeRefsReducer, savedAssetRefs);
await savedObjectsClient.create(
PACKAGES_SAVED_OBJECT_TYPE,
{
- installed: toInstallAssetsRefs,
- es_index_patterns: toInstallESIndexPatterns,
+ installed: toSaveAssetRefs,
+ es_index_patterns: toSaveESIndexPatterns,
name: pkgName,
version: pkgVersion,
internal,
@@ -195,7 +224,7 @@ export async function saveInstallationReferences(options: {
{ id: pkgName, overwrite: true }
);
- return toInstallAssetsRefs;
+ return toSaveAssetRefs;
}
async function installKibanaSavedObjects({
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
index a30acb97b99cf0..ed7b7f33013277 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts
@@ -29,7 +29,17 @@ export async function removeInstallation(options: {
// recreate or delete index patterns when a package is uninstalled
await installIndexPatterns(savedObjectsClient);
- // Delete the installed assets
+ // Delete the installed asset
+ await deleteAssets(installedObjects, savedObjectsClient, callCluster);
+
+ // successful delete's in SO client return {}. return something more useful
+ return installedObjects;
+}
+async function deleteAssets(
+ installedObjects: AssetReference[],
+ savedObjectsClient: SavedObjectsClientContract,
+ callCluster: CallESAsCurrentUser
+) {
const deletePromises = installedObjects.map(async ({ id, type }) => {
const assetType = type as AssetType;
if (savedObjectTypes.includes(assetType)) {
@@ -40,22 +50,62 @@ export async function removeInstallation(options: {
deleteTemplate(callCluster, id);
}
});
- await Promise.all([...deletePromises]);
-
- // successful delete's in SO client return {}. return something more useful
- return installedObjects;
+ try {
+ await Promise.all([...deletePromises]);
+ } catch (err) {
+ throw new Error(err.message);
+ }
}
-
async function deletePipeline(callCluster: CallESAsCurrentUser, id: string): Promise {
// '*' shouldn't ever appear here, but it still would delete all ingest pipelines
if (id && id !== '*') {
- await callCluster('ingest.deletePipeline', { id });
+ try {
+ await callCluster('ingest.deletePipeline', { id });
+ } catch (err) {
+ throw new Error(`error deleting pipeline ${id}`);
+ }
}
}
async function deleteTemplate(callCluster: CallESAsCurrentUser, name: string): Promise {
// '*' shouldn't ever appear here, but it still would delete all templates
if (name && name !== '*') {
- await callCluster('indices.deleteTemplate', { name });
+ try {
+ await callCluster('indices.deleteTemplate', { name });
+ } catch {
+ throw new Error(`error deleting template ${name}`);
+ }
}
}
+
+export async function deleteAssetsByType({
+ savedObjectsClient,
+ callCluster,
+ installedObjects,
+ assetType,
+}: {
+ savedObjectsClient: SavedObjectsClientContract;
+ callCluster: CallESAsCurrentUser;
+ installedObjects: AssetReference[];
+ assetType: ElasticsearchAssetType;
+}) {
+ const toDelete = installedObjects.filter(asset => asset.type === assetType);
+ try {
+ await deleteAssets(toDelete, savedObjectsClient, callCluster);
+ } catch (err) {
+ throw new Error(err.message);
+ }
+}
+
+export async function deleteKibanaSavedObjectsAssets(
+ savedObjectsClient: SavedObjectsClientContract,
+ installedObjects: AssetReference[]
+) {
+ const deletePromises = installedObjects.map(({ id, type }) => {
+ const assetType = type as AssetType;
+ if (savedObjectTypes.includes(assetType)) {
+ savedObjectsClient.delete(assetType, id);
+ }
+ });
+ await Promise.all(deletePromises);
+}
diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx
index 144929a385b956..aa5496cc836b71 100644
--- a/x-pack/plugins/ingest_manager/server/types/index.tsx
+++ b/x-pack/plugins/ingest_manager/server/types/index.tsx
@@ -22,6 +22,7 @@ export {
AgentConfig,
NewAgentConfig,
AgentConfigStatus,
+ DataStream,
Output,
NewOutput,
OutputType,
@@ -47,7 +48,8 @@ export {
RegistrySearchResults,
RegistrySearchResult,
DefaultPackages,
- DataStream,
+ TemplateRef,
+ IndexTemplateMappings,
} from '../../common';
export type CallESAsCurrentUser = ScopedClusterClient['callAsCurrentUser'];
diff --git a/x-pack/plugins/maps/public/actions/map_actions.d.ts b/x-pack/plugins/maps/public/actions/map_actions.d.ts
index debead3ad5c45d..c8db284a5c4f1a 100644
--- a/x-pack/plugins/maps/public/actions/map_actions.d.ts
+++ b/x-pack/plugins/maps/public/actions/map_actions.d.ts
@@ -14,6 +14,7 @@ import {
MapCenterAndZoom,
MapRefreshConfig,
} from '../../common/descriptor_types';
+import { MapSettings } from '../reducers/map';
export type SyncContext = {
startLoading(dataId: string, requestToken: symbol, meta: DataMeta): void;
@@ -62,3 +63,14 @@ export function hideViewControl(): AnyAction;
export function setHiddenLayers(hiddenLayerIds: string[]): AnyAction;
export function addLayerWithoutDataSync(layerDescriptor: unknown): AnyAction;
+
+export function setMapSettings(settings: MapSettings): AnyAction;
+
+export function rollbackMapSettings(): AnyAction;
+
+export function trackMapSettings(): AnyAction;
+
+export function updateMapSetting(
+ settingKey: string,
+ settingValue: string | boolean | number
+): AnyAction;
diff --git a/x-pack/plugins/maps/public/actions/map_actions.js b/x-pack/plugins/maps/public/actions/map_actions.js
index 572385d628b16a..da6ba6b481054b 100644
--- a/x-pack/plugins/maps/public/actions/map_actions.js
+++ b/x-pack/plugins/maps/public/actions/map_actions.js
@@ -76,6 +76,10 @@ export const HIDE_TOOLBAR_OVERLAY = 'HIDE_TOOLBAR_OVERLAY';
export const HIDE_LAYER_CONTROL = 'HIDE_LAYER_CONTROL';
export const HIDE_VIEW_CONTROL = 'HIDE_VIEW_CONTROL';
export const SET_WAITING_FOR_READY_HIDDEN_LAYERS = 'SET_WAITING_FOR_READY_HIDDEN_LAYERS';
+export const SET_MAP_SETTINGS = 'SET_MAP_SETTINGS';
+export const ROLLBACK_MAP_SETTINGS = 'ROLLBACK_MAP_SETTINGS';
+export const TRACK_MAP_SETTINGS = 'TRACK_MAP_SETTINGS';
+export const UPDATE_MAP_SETTING = 'UPDATE_MAP_SETTING';
function getLayerLoadingCallbacks(dispatch, getState, layerId) {
return {
@@ -145,6 +149,29 @@ export function setMapInitError(errorMessage) {
};
}
+export function setMapSettings(settings) {
+ return {
+ type: SET_MAP_SETTINGS,
+ settings,
+ };
+}
+
+export function rollbackMapSettings() {
+ return { type: ROLLBACK_MAP_SETTINGS };
+}
+
+export function trackMapSettings() {
+ return { type: TRACK_MAP_SETTINGS };
+}
+
+export function updateMapSetting(settingKey, settingValue) {
+ return {
+ type: UPDATE_MAP_SETTING,
+ settingKey,
+ settingValue,
+ };
+}
+
export function trackCurrentLayerState(layerId) {
return {
type: TRACK_CURRENT_LAYER_STATE,
diff --git a/x-pack/plugins/maps/public/actions/ui_actions.d.ts b/x-pack/plugins/maps/public/actions/ui_actions.d.ts
index e087dc70256f06..43cdcff7d2d697 100644
--- a/x-pack/plugins/maps/public/actions/ui_actions.d.ts
+++ b/x-pack/plugins/maps/public/actions/ui_actions.d.ts
@@ -5,6 +5,7 @@
*/
import { AnyAction } from 'redux';
+import { FLYOUT_STATE } from '../reducers/ui';
export const UPDATE_FLYOUT: string;
export const CLOSE_SET_VIEW: string;
@@ -17,6 +18,8 @@ export const SHOW_TOC_DETAILS: string;
export const HIDE_TOC_DETAILS: string;
export const UPDATE_INDEXING_STAGE: string;
+export function updateFlyout(display: FLYOUT_STATE): AnyAction;
+
export function setOpenTOCDetails(layerIds?: string[]): AnyAction;
export function setIsLayerTOCOpen(open: boolean): AnyAction;
diff --git a/x-pack/plugins/maps/public/actions/ui_actions.js b/x-pack/plugins/maps/public/actions/ui_actions.js
index 77fdf6b0f12d23..e2a36e33e7db09 100644
--- a/x-pack/plugins/maps/public/actions/ui_actions.js
+++ b/x-pack/plugins/maps/public/actions/ui_actions.js
@@ -4,6 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { getFlyoutDisplay } from '../selectors/ui_selectors';
+import { FLYOUT_STATE } from '../reducers/ui';
+import { setSelectedLayer, trackMapSettings } from './map_actions';
+
export const UPDATE_FLYOUT = 'UPDATE_FLYOUT';
export const CLOSE_SET_VIEW = 'CLOSE_SET_VIEW';
export const OPEN_SET_VIEW = 'OPEN_SET_VIEW';
@@ -28,6 +32,17 @@ export function updateFlyout(display) {
display,
};
}
+export function openMapSettings() {
+ return (dispatch, getState) => {
+ const flyoutDisplay = getFlyoutDisplay(getState());
+ if (flyoutDisplay === FLYOUT_STATE.MAP_SETTINGS_PANEL) {
+ return;
+ }
+ dispatch(setSelectedLayer(null));
+ dispatch(trackMapSettings());
+ dispatch(updateFlyout(FLYOUT_STATE.MAP_SETTINGS_PANEL));
+ };
+}
export function closeSetView() {
return {
type: CLOSE_SET_VIEW,
diff --git a/x-pack/plugins/maps/public/angular/services/saved_gis_map.js b/x-pack/plugins/maps/public/angular/services/saved_gis_map.js
index 1c47e0ab7dc2a4..1a58b0cefaed97 100644
--- a/x-pack/plugins/maps/public/angular/services/saved_gis_map.js
+++ b/x-pack/plugins/maps/public/angular/services/saved_gis_map.js
@@ -15,6 +15,7 @@ import {
getRefreshConfig,
getQuery,
getFilters,
+ getMapSettings,
} from '../../selectors/map_selectors';
import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../selectors/ui_selectors';
@@ -98,6 +99,7 @@ export function createSavedGisMapClass(services) {
refreshConfig: getRefreshConfig(state),
query: _.omit(getQuery(state), 'queryLastTriggeredAt'),
filters: getFilters(state),
+ settings: getMapSettings(state),
});
this.uiStateJSON = JSON.stringify({
diff --git a/x-pack/plugins/maps/public/connected_components/gis_map/index.js b/x-pack/plugins/maps/public/connected_components/gis_map/index.js
index c825fdab75ca74..f8769d0bb898ad 100644
--- a/x-pack/plugins/maps/public/connected_components/gis_map/index.js
+++ b/x-pack/plugins/maps/public/connected_components/gis_map/index.js
@@ -6,8 +6,6 @@
import { connect } from 'react-redux';
import { GisMap } from './view';
-
-import { FLYOUT_STATE } from '../../reducers/ui';
import { exitFullScreen } from '../../actions/ui_actions';
import { getFlyoutDisplay, getIsFullScreen } from '../../selectors/ui_selectors';
import { triggerRefreshTimer, cancelAllInFlightRequests } from '../../actions/map_actions';
@@ -22,12 +20,9 @@ import {
import { getCoreChrome } from '../../kibana_services';
function mapStateToProps(state = {}) {
- const flyoutDisplay = getFlyoutDisplay(state);
return {
areLayersLoaded: areLayersLoaded(state),
- layerDetailsVisible: flyoutDisplay === FLYOUT_STATE.LAYER_PANEL,
- addLayerVisible: flyoutDisplay === FLYOUT_STATE.ADD_LAYER_WIZARD,
- noFlyoutVisible: flyoutDisplay === FLYOUT_STATE.NONE,
+ flyoutDisplay: getFlyoutDisplay(state),
isFullScreen: getIsFullScreen(state),
refreshConfig: getRefreshConfig(state),
mapInitError: getMapInitError(state),
diff --git a/x-pack/plugins/maps/public/connected_components/gis_map/view.js b/x-pack/plugins/maps/public/connected_components/gis_map/view.js
index 28ad12133d6118..6eb173a001d018 100644
--- a/x-pack/plugins/maps/public/connected_components/gis_map/view.js
+++ b/x-pack/plugins/maps/public/connected_components/gis_map/view.js
@@ -6,6 +6,7 @@
import _ from 'lodash';
import React, { Component } from 'react';
+import classNames from 'classnames';
import { MBMapContainer } from '../map/mb';
import { WidgetOverlay } from '../widget_overlay';
import { ToolbarOverlay } from '../toolbar_overlay';
@@ -19,6 +20,8 @@ import { ES_GEO_FIELD_TYPE } from '../../../common/constants';
import { indexPatterns as indexPatternsUtils } from '../../../../../../src/plugins/data/public';
import { i18n } from '@kbn/i18n';
import uuid from 'uuid/v4';
+import { FLYOUT_STATE } from '../../reducers/ui';
+import { MapSettingsPanel } from '../map_settings_panel';
const RENDER_COMPLETE_EVENT = 'renderComplete';
@@ -147,9 +150,7 @@ export class GisMap extends Component {
render() {
const {
addFilters,
- layerDetailsVisible,
- addLayerVisible,
- noFlyoutVisible,
+ flyoutDisplay,
isFullScreen,
exitFullScreen,
mapInitError,
@@ -174,16 +175,13 @@ export class GisMap extends Component {
);
}
- let currentPanel;
- let currentPanelClassName;
- if (noFlyoutVisible) {
- currentPanel = null;
- } else if (addLayerVisible) {
- currentPanelClassName = 'mapMapLayerPanel-isVisible';
- currentPanel = ;
- } else if (layerDetailsVisible) {
- currentPanelClassName = 'mapMapLayerPanel-isVisible';
- currentPanel = ;
+ let flyoutPanel = null;
+ if (flyoutDisplay === FLYOUT_STATE.ADD_LAYER_WIZARD) {
+ flyoutPanel = ;
+ } else if (flyoutDisplay === FLYOUT_STATE.LAYER_PANEL) {
+ flyoutPanel = ;
+ } else if (flyoutDisplay === FLYOUT_STATE.MAP_SETTINGS_PANEL) {
+ flyoutPanel = ;
}
let exitFullScreenButton;
@@ -210,8 +208,13 @@ export class GisMap extends Component {
-
- {currentPanel}
+
+ {flyoutPanel}
{exitFullScreenButton}
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/index.js b/x-pack/plugins/maps/public/connected_components/map/mb/index.js
index d864b60eb433bf..459b38d4226948 100644
--- a/x-pack/plugins/maps/public/connected_components/map/mb/index.js
+++ b/x-pack/plugins/maps/public/connected_components/map/mb/index.js
@@ -23,6 +23,7 @@ import {
isInteractiveDisabled,
isTooltipControlDisabled,
isViewControlHidden,
+ getMapSettings,
} from '../../../selectors/map_selectors';
import { getInspectorAdapters } from '../../../reducers/non_serializable_instances';
@@ -30,6 +31,7 @@ import { getInspectorAdapters } from '../../../reducers/non_serializable_instanc
function mapStateToProps(state = {}) {
return {
isMapReady: getMapReady(state),
+ settings: getMapSettings(state),
layerList: getLayerList(state),
goto: getGoto(state),
inspectorAdapters: getInspectorAdapters(state),
diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/plugins/maps/public/connected_components/map/mb/view.js
index 2d95de184f0f4e..71c1af44e493bd 100644
--- a/x-pack/plugins/maps/public/connected_components/map/mb/view.js
+++ b/x-pack/plugins/maps/public/connected_components/map/mb/view.js
@@ -12,14 +12,8 @@ import {
removeOrphanedSourcesAndLayers,
addSpritesheetToMap,
} from './utils';
-
import { getGlyphUrl, isRetina } from '../../../meta';
-import {
- DECIMAL_DEGREES_PRECISION,
- MAX_ZOOM,
- MIN_ZOOM,
- ZOOM_PRECISION,
-} from '../../../../common/constants';
+import { DECIMAL_DEGREES_PRECISION, ZOOM_PRECISION } from '../../../../common/constants';
import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp';
import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker';
import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js';
@@ -80,7 +74,7 @@ export class MBMapContainer extends React.Component {
}
_debouncedSync = _.debounce(() => {
- if (this._isMounted) {
+ if (this._isMounted || !this.props.isMapReady) {
if (!this.state.hasSyncedLayerList) {
this.setState(
{
@@ -92,6 +86,7 @@ export class MBMapContainer extends React.Component {
}
);
}
+ this._syncSettings();
}
}, 256);
@@ -133,8 +128,8 @@ export class MBMapContainer extends React.Component {
scrollZoom: this.props.scrollZoom,
preserveDrawingBuffer: getInjectedVarFunc()('preserveDrawingBuffer', false),
interactive: !this.props.disableInteractive,
- minZoom: MIN_ZOOM,
- maxZoom: MAX_ZOOM,
+ maxZoom: this.props.settings.maxZoom,
+ minZoom: this.props.settings.minZoom,
};
const initialView = _.get(this.props.goto, 'center');
if (initialView) {
@@ -265,17 +260,13 @@ export class MBMapContainer extends React.Component {
};
_syncMbMapWithLayerList = () => {
- if (!this.props.isMapReady) {
- return;
- }
-
removeOrphanedSourcesAndLayers(this.state.mbMap, this.props.layerList);
this.props.layerList.forEach(layer => layer.syncLayerWithMB(this.state.mbMap));
syncLayerOrderForSingleLayer(this.state.mbMap, this.props.layerList);
};
_syncMbMapWithInspector = () => {
- if (!this.props.isMapReady || !this.props.inspectorAdapters.map) {
+ if (!this.props.inspectorAdapters.map) {
return;
}
@@ -289,6 +280,27 @@ export class MBMapContainer extends React.Component {
});
};
+ _syncSettings() {
+ let zoomRangeChanged = false;
+ if (this.props.settings.minZoom !== this.state.mbMap.getMinZoom()) {
+ this.state.mbMap.setMinZoom(this.props.settings.minZoom);
+ zoomRangeChanged = true;
+ }
+ if (this.props.settings.maxZoom !== this.state.mbMap.getMaxZoom()) {
+ this.state.mbMap.setMaxZoom(this.props.settings.maxZoom);
+ zoomRangeChanged = true;
+ }
+
+ // 'moveend' event not fired when map moves from setMinZoom or setMaxZoom
+ // https://github.com/mapbox/mapbox-gl-js/issues/9610
+ // hack to update extent after zoom update finishes moving map.
+ if (zoomRangeChanged) {
+ setTimeout(() => {
+ this.props.extentChanged(this._getMapState());
+ }, 300);
+ }
+ }
+
render() {
let drawControl;
let tooltipControl;
diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts b/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts
new file mode 100644
index 00000000000000..329fac28d7d2ee
--- /dev/null
+++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { AnyAction, Dispatch } from 'redux';
+import { connect } from 'react-redux';
+import { FLYOUT_STATE } from '../../reducers/ui';
+import { MapStoreState } from '../../reducers/store';
+import { MapSettingsPanel } from './map_settings_panel';
+import { rollbackMapSettings, updateMapSetting } from '../../actions/map_actions';
+import { getMapSettings, hasMapSettingsChanges } from '../../selectors/map_selectors';
+import { updateFlyout } from '../../actions/ui_actions';
+
+function mapStateToProps(state: MapStoreState) {
+ return {
+ settings: getMapSettings(state),
+ hasMapSettingsChanges: hasMapSettingsChanges(state),
+ };
+}
+
+function mapDispatchToProps(dispatch: Dispatch) {
+ return {
+ cancelChanges: () => {
+ dispatch(rollbackMapSettings());
+ dispatch(updateFlyout(FLYOUT_STATE.NONE));
+ },
+ keepChanges: () => {
+ dispatch(updateFlyout(FLYOUT_STATE.NONE));
+ },
+ updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => {
+ dispatch(updateMapSetting(settingKey, settingValue));
+ },
+ };
+}
+
+const connectedMapSettingsPanel = connect(mapStateToProps, mapDispatchToProps)(MapSettingsPanel);
+export { connectedMapSettingsPanel as MapSettingsPanel };
diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx
new file mode 100644
index 00000000000000..36ed29e92cf69a
--- /dev/null
+++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.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 React from 'react';
+import {
+ EuiButton,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiFlyoutFooter,
+ EuiFlyoutHeader,
+ EuiSpacer,
+ EuiTitle,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { MapSettings } from '../../reducers/map';
+import { NavigationPanel } from './navigation_panel';
+
+interface Props {
+ cancelChanges: () => void;
+ hasMapSettingsChanges: boolean;
+ keepChanges: () => void;
+ settings: MapSettings;
+ updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => void;
+}
+
+export function MapSettingsPanel({
+ cancelChanges,
+ hasMapSettingsChanges,
+ keepChanges,
+ settings,
+ updateMapSetting,
+}: Props) {
+ // TODO move common text like Cancel and Close to common i18n translation
+ const closeBtnLabel = hasMapSettingsChanges
+ ? i18n.translate('xpack.maps.mapSettingsPanel.cancelLabel', {
+ defaultMessage: 'Cancel',
+ })
+ : i18n.translate('xpack.maps.mapSettingsPanel.closeLabel', {
+ defaultMessage: 'Close',
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {closeBtnLabel}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.tsx
new file mode 100644
index 00000000000000..ed83e838f44f6f
--- /dev/null
+++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.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 React from 'react';
+import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { MapSettings } from '../../reducers/map';
+import { ValidatedDualRange, Value } from '../../../../../../src/plugins/kibana_react/public';
+import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants';
+
+interface Props {
+ settings: MapSettings;
+ updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => void;
+}
+
+export function NavigationPanel({ settings, updateMapSetting }: Props) {
+ const onZoomChange = (value: Value) => {
+ updateMapSetting('minZoom', Math.max(MIN_ZOOM, parseInt(value[0] as string, 10)));
+ updateMapSetting('maxZoom', Math.min(MAX_ZOOM, parseInt(value[1] as string, 10)));
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js
index 2b6fae26098beb..c3cc4090ab9522 100644
--- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js
+++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js
@@ -7,12 +7,13 @@
import { connect } from 'react-redux';
import { SetViewControl } from './set_view_control';
import { setGotoWithCenter } from '../../../actions/map_actions';
-import { getMapZoom, getMapCenter } from '../../../selectors/map_selectors';
+import { getMapZoom, getMapCenter, getMapSettings } from '../../../selectors/map_selectors';
import { closeSetView, openSetView } from '../../../actions/ui_actions';
import { getIsSetViewOpen } from '../../../selectors/ui_selectors';
function mapStateToProps(state = {}) {
return {
+ settings: getMapSettings(state),
isSetViewOpen: getIsSetViewOpen(state),
zoom: getMapZoom(state),
center: getMapCenter(state),
diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js
index 9c983447bfbf62..2c10728f78e5c5 100644
--- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js
+++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js
@@ -18,7 +18,6 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants';
function getViewString(lat, lon, zoom) {
return `${lat},${lon},${zoom}`;
@@ -118,8 +117,8 @@ export class SetViewControl extends Component {
const { isInvalid: isZoomInvalid, component: zoomFormRow } = this._renderNumberFormRow({
value: this.state.zoom,
- min: MIN_ZOOM,
- max: MAX_ZOOM,
+ min: this.props.settings.minZoom,
+ max: this.props.settings.maxZoom,
onChange: this._onZoomChange,
label: i18n.translate('xpack.maps.setViewControl.zoomLabel', {
defaultMessage: 'Zoom',
diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap
index 560ebad89c50ea..0af4eb0793f035 100644
--- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap
+++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap
@@ -65,7 +65,7 @@ exports[`LayerControl is rendered 1`] = `
data-test-subj="addLayerButton"
fill={true}
fullWidth={true}
- isDisabled={true}
+ isDisabled={false}
onClick={[Function]}
>
`;
+
+exports[`LayerControl should disable buttons when flyout is open 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js
index 8780bac59e4b72..915f808b8e3589 100644
--- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js
+++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js
@@ -22,7 +22,7 @@ function mapStateToProps(state = {}) {
isReadOnly: getIsReadOnly(state),
isLayerTOCOpen: getIsLayerTOCOpen(state),
layerList: getLayerList(state),
- isAddButtonActive: getFlyoutDisplay(state) === FLYOUT_STATE.NONE,
+ isFlyoutOpen: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE,
};
}
diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js
index 537a676287042f..180dc2e3933c36 100644
--- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js
+++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js
@@ -57,7 +57,7 @@ export function LayerControl({
closeLayerTOC,
openLayerTOC,
layerList,
- isAddButtonActive,
+ isFlyoutOpen,
}) {
if (!isLayerTOCOpen) {
const hasErrors = layerList.some(layer => {
@@ -86,7 +86,7 @@ export function LayerControl({
{},
isLayerTOCOpen: true,
layerList: [],
+ isFlyoutOpen: false,
};
describe('LayerControl', () => {
@@ -30,6 +31,12 @@ describe('LayerControl', () => {
expect(component).toMatchSnapshot();
});
+ test('should disable buttons when flyout is open', () => {
+ const component = shallow();
+
+ expect(component).toMatchSnapshot();
+ });
+
test('isReadOnly', () => {
const component = shallow();
diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
index dbd48d614e99b4..467cf4727edb78 100644
--- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
+++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx
@@ -28,6 +28,7 @@ import {
} from '../../../../../src/plugins/data/public';
import { GisMap } from '../connected_components/gis_map';
import { createMapStore, MapStore } from '../reducers/store';
+import { MapSettings } from '../reducers/map';
import {
setGotoWithCenter,
replaceLayerList,
@@ -40,6 +41,7 @@ import {
hideLayerControl,
hideViewControl,
setHiddenLayers,
+ setMapSettings,
} from '../actions/map_actions';
import { MapCenterAndZoom } from '../../common/descriptor_types';
import { setReadOnly, setIsLayerTOCOpen, setOpenTOCDetails } from '../actions/ui_actions';
@@ -60,6 +62,7 @@ interface MapEmbeddableConfig {
editable: boolean;
title?: string;
layerList: unknown[];
+ settings?: MapSettings;
}
export interface MapEmbeddableInput extends EmbeddableInput {
@@ -97,6 +100,7 @@ export class MapEmbeddable extends Embeddable this.onContainerStateChanged(input));
@@ -194,6 +199,10 @@ export class MapEmbeddable extends Embeddable map.settings;
+
+const getRollbackMapSettings = ({ map }) => map.__rollbackSettings;
+
+export const hasMapSettingsChanges = createSelector(
+ getMapSettings,
+ getRollbackMapSettings,
+ (settings, rollbackSettings) => {
+ return rollbackSettings ? !_.isEqual(settings, rollbackSettings) : false;
+ }
+);
+
export const getOpenTooltips = ({ map }) => {
return map && map.openTooltips ? map.openTooltips : [];
};
diff --git a/x-pack/plugins/reporting/common/types.d.ts b/x-pack/plugins/reporting/common/types.d.ts
new file mode 100644
index 00000000000000..34f0bc9ac8a366
--- /dev/null
+++ b/x-pack/plugins/reporting/common/types.d.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 { ConfigType } from '../server/config';
diff --git a/x-pack/plugins/reporting/constants.ts b/x-pack/plugins/reporting/constants.ts
index 5756d29face12a..8079c5b1d98875 100644
--- a/x-pack/plugins/reporting/constants.ts
+++ b/x-pack/plugins/reporting/constants.ts
@@ -7,13 +7,6 @@
export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY =
'xpack.reporting.jobCompletionNotifications';
-export const JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG = {
- jobCompletionNotifier: {
- interval: 10000,
- intervalErrorMultiplier: 5,
- },
-};
-
// Routes
export const API_BASE_URL = '/api/reporting';
export const API_LIST_URL = `${API_BASE_URL}/jobs`;
diff --git a/x-pack/plugins/reporting/index.d.ts b/x-pack/plugins/reporting/index.d.ts
index 26d661e29bd946..77faf837e65058 100644
--- a/x-pack/plugins/reporting/index.d.ts
+++ b/x-pack/plugins/reporting/index.d.ts
@@ -4,15 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- CoreSetup,
- CoreStart,
- HttpSetup,
- Plugin,
- PluginInitializerContext,
- NotificationsStart,
-} from '../../../src/core/public';
-
export type JobId = string;
export type JobStatus =
| 'completed'
@@ -21,9 +12,6 @@ export type JobStatus =
| 'processing'
| 'failed';
-export type HttpService = HttpSetup;
-export type NotificationsService = NotificationsStart;
-
export interface SourceJob {
_id: JobId;
_source: {
diff --git a/x-pack/plugins/reporting/public/components/report_listing.test.tsx b/x-pack/plugins/reporting/public/components/report_listing.test.tsx
index 380a3b3295b9f1..787279e6caf9b1 100644
--- a/x-pack/plugins/reporting/public/components/report_listing.test.tsx
+++ b/x-pack/plugins/reporting/public/components/report_listing.test.tsx
@@ -47,12 +47,24 @@ const toasts = {
addDanger: jest.fn(),
} as any;
+const mockPollConfig = {
+ jobCompletionNotifier: {
+ interval: 5000,
+ intervalErrorMultiplier: 3,
+ },
+ jobsRefresh: {
+ interval: 5000,
+ intervalErrorMultiplier: 3,
+ },
+};
+
describe('ReportListing', () => {
it('Report job listing with some items', () => {
const wrapper = mountWithIntl(
@@ -74,6 +86,7 @@ describe('ReportListing', () => {
}
+ pollConfig={mockPollConfig}
redirect={jest.fn()}
toasts={toasts}
/>
diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx
index 885e9577471a05..d8f9b7d37cfbf7 100644
--- a/x-pack/plugins/reporting/public/components/report_listing.tsx
+++ b/x-pack/plugins/reporting/public/components/report_listing.tsx
@@ -16,14 +16,15 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { get } from 'lodash';
import moment from 'moment';
-import { Component, Fragment, default as React } from 'react';
+import { Component, default as React, Fragment } from 'react';
import { Subscription } from 'rxjs';
import { ApplicationStart, ToastsSetup } from 'src/core/public';
import { ILicense, LicensingPluginSetup } from '../../../licensing/public';
import { Poller } from '../../common/poller';
-import { JobStatuses, JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG } from '../../constants';
+import { JobStatuses } from '../../constants';
import { checkLicense } from '../lib/license_check';
import { JobQueueEntry, ReportingAPIClient } from '../lib/reporting_api_client';
+import { ClientConfigType } from '../plugin';
import {
ReportDeleteButton,
ReportDownloadButton,
@@ -53,6 +54,7 @@ export interface Props {
intl: InjectedIntl;
apiClient: ReportingAPIClient;
license$: LicensingPluginSetup['license$'];
+ pollConfig: ClientConfigType['poll'];
redirect: ApplicationStart['navigateToApp'];
toasts: ToastsSetup;
}
@@ -167,12 +169,10 @@ class ReportListingUi extends Component {
functionToPoll: () => {
return this.fetchJobs();
},
- pollFrequencyInMillis:
- JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG.jobCompletionNotifier.interval,
+ pollFrequencyInMillis: this.props.pollConfig.jobsRefresh.interval,
trailing: false,
continuePollingOnError: true,
- pollFrequencyErrorMultiplier:
- JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG.jobCompletionNotifier.intervalErrorMultiplier,
+ pollFrequencyErrorMultiplier: this.props.pollConfig.jobsRefresh.intervalErrorMultiplier,
});
this.poller.start();
this.licenseSubscription = this.props.license$.subscribe(this.licenseHandler);
diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts
index 3c121f1712685b..eed6d5dd141e70 100644
--- a/x-pack/plugins/reporting/public/lib/stream_handler.ts
+++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts
@@ -4,30 +4,23 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { i18n } from '@kbn/i18n';
import * as Rx from 'rxjs';
import { catchError, map } from 'rxjs/operators';
-import { i18n } from '@kbn/i18n';
+import { NotificationsSetup } from 'src/core/public';
import {
JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY,
JOB_STATUS_COMPLETED,
JOB_STATUS_FAILED,
JOB_STATUS_WARNINGS,
} from '../../constants';
-
-import {
- JobId,
- JobSummary,
- JobStatusBuckets,
- NotificationsService,
- SourceJob,
-} from '../../index.d';
-
+import { JobId, JobStatusBuckets, JobSummary, SourceJob } from '../../index.d';
import {
- getSuccessToast,
getFailureToast,
+ getGeneralErrorToast,
+ getSuccessToast,
getWarningFormulasToast,
getWarningMaxSizeToast,
- getGeneralErrorToast,
} from '../components';
import { ReportingAPIClient } from './reporting_api_client';
@@ -47,7 +40,7 @@ function summarizeJob(src: SourceJob): JobSummary {
}
export class ReportingNotifierStreamHandler {
- constructor(private notifications: NotificationsService, private apiClient: ReportingAPIClient) {}
+ constructor(private notifications: NotificationsSetup, private apiClient: ReportingAPIClient) {}
/*
* Use Kibana Toast API to show our messages
diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.tsx
index 08ba10ff692078..c40e7ad373eafa 100644
--- a/x-pack/plugins/reporting/public/plugin.tsx
+++ b/x-pack/plugins/reporting/public/plugin.tsx
@@ -4,44 +4,42 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import * as Rx from 'rxjs';
+import { i18n } from '@kbn/i18n';
+import { I18nProvider } from '@kbn/i18n/react';
import React from 'react';
import ReactDOM from 'react-dom';
+import * as Rx from 'rxjs';
import { catchError, filter, map, mergeMap, takeUntil } from 'rxjs/operators';
-import { i18n } from '@kbn/i18n';
+import {
+ CoreSetup,
+ CoreStart,
+ NotificationsSetup,
+ Plugin,
+ PluginInitializerContext,
+} from 'src/core/public';
import { ManagementSetup } from 'src/plugins/management/public';
-import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public';
-import { I18nProvider } from '@kbn/i18n/react';
import { UiActionsSetup } from 'src/plugins/ui_actions/public';
-
-import { ReportListing } from './components/report_listing';
-import { getGeneralErrorToast } from './components';
-
-import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler';
-import { ReportingAPIClient } from './lib/reporting_api_client';
-import { GetCsvReportPanelAction } from './panel_actions/get_csv_panel_action';
-import { csvReportingProvider } from './share_context_menu/register_csv_reporting';
-import { reportingPDFPNGProvider } from './share_context_menu/register_pdf_png_reporting';
-
-import { LicensingPluginSetup } from '../../licensing/public';
+import { JobId, JobStatusBuckets } from '../';
import { CONTEXT_MENU_TRIGGER } from '../../../../src/plugins/embeddable/public';
-import { SharePluginSetup } from '../../../../src/plugins/share/public';
-
import {
FeatureCatalogueCategory,
HomePublicPluginSetup,
} from '../../../../src/plugins/home/public';
+import { SharePluginSetup } from '../../../../src/plugins/share/public';
+import { LicensingPluginSetup } from '../../licensing/public';
+import { ConfigType } from '../common/types';
+import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../constants';
+import { getGeneralErrorToast } from './components';
+import { ReportListing } from './components/report_listing';
+import { ReportingAPIClient } from './lib/reporting_api_client';
+import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler';
+import { GetCsvReportPanelAction } from './panel_actions/get_csv_panel_action';
+import { csvReportingProvider } from './share_context_menu/register_csv_reporting';
+import { reportingPDFPNGProvider } from './share_context_menu/register_pdf_png_reporting';
-import {
- JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG,
- JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY,
-} from '../constants';
-
-import { JobId, JobStatusBuckets, NotificationsService } from '..';
-
-const {
- jobCompletionNotifier: { interval: JOBS_REFRESH_INTERVAL },
-} = JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG;
+export interface ClientConfigType {
+ poll: ConfigType['poll'];
+}
function getStored(): JobId[] {
const sessionValue = sessionStorage.getItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY);
@@ -49,7 +47,7 @@ function getStored(): JobId[] {
}
function handleError(
- notifications: NotificationsService,
+ notifications: NotificationsSetup,
err: Error
): Rx.Observable {
notifications.toasts.addDanger(
@@ -64,18 +62,19 @@ function handleError(
return Rx.of({ completed: [], failed: [] });
}
-export class ReportingPublicPlugin implements Plugin {
+export class ReportingPublicPlugin implements Plugin {
+ private config: ClientConfigType;
private readonly stop$ = new Rx.ReplaySubject(1);
-
private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', {
defaultMessage: 'Reporting',
});
-
private readonly breadcrumbText = i18n.translate('xpack.reporting.breadcrumb', {
defaultMessage: 'Reporting',
});
- constructor(initializerContext: PluginInitializerContext) {}
+ constructor(initializerContext: PluginInitializerContext) {
+ this.config = initializerContext.config.get();
+ }
public setup(
core: CoreSetup,
@@ -130,6 +129,7 @@ export class ReportingPublicPlugin implements Plugin {
@@ -163,8 +163,9 @@ export class ReportingPublicPlugin implements Plugin {
const { http, notifications } = core;
const apiClient = new ReportingAPIClient(http);
const streamHandler = new StreamHandler(notifications, apiClient);
+ const { interval } = this.config.poll.jobsRefresh;
- Rx.timer(0, JOBS_REFRESH_INTERVAL)
+ Rx.timer(0, interval)
.pipe(
takeUntil(this.stop$), // stop the interval when stop method is called
map(() => getStored()), // read all pending job IDs from session storage
diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts
index f0a0a093aa8c08..a0d7618322c653 100644
--- a/x-pack/plugins/reporting/server/config/index.ts
+++ b/x-pack/plugins/reporting/server/config/index.ts
@@ -10,6 +10,7 @@ import { ConfigSchema, ConfigType } from './schema';
export { createConfig$ } from './create_config';
export const config: PluginConfigDescriptor = {
+ exposeToBrowser: { poll: true },
schema: ConfigSchema,
deprecations: ({ unused }) => [
unused('capture.browser.chromium.maxScreenshotDimension'),
diff --git a/x-pack/plugins/siem/common/default_index_pattern.ts b/x-pack/plugins/siem/common/default_index_pattern.ts
deleted file mode 100644
index 4d53aeb000c557..00000000000000
--- a/x-pack/plugins/siem/common/default_index_pattern.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.
- */
-
-/** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */
-export const defaultIndexPattern = [
- 'apm-*-transaction*',
- 'auditbeat-*',
- 'endgame-*',
- 'filebeat-*',
- 'packetbeat-*',
- 'winlogbeat-*',
-];
diff --git a/x-pack/plugins/siem/common/utility_types.ts b/x-pack/plugins/siem/common/utility_types.ts
index c7bbdbfccf0822..b46ccdbbe3d05a 100644
--- a/x-pack/plugins/siem/common/utility_types.ts
+++ b/x-pack/plugins/siem/common/utility_types.ts
@@ -6,12 +6,6 @@
import { ReactNode } from 'react';
-export type Pick3 = {
- [P1 in K1]: { [P2 in K2]: { [P3 in K3]: T[K1][K2][P3] } };
-};
-
-export type Omit = Pick>;
-
// This type is for typing EuiDescriptionList
export interface DescriptionList {
title: NonNullable;
diff --git a/x-pack/plugins/siem/package.json b/x-pack/plugins/siem/package.json
index 1fcef46243628c..31c930dce71c06 100644
--- a/x-pack/plugins/siem/package.json
+++ b/x-pack/plugins/siem/package.json
@@ -5,7 +5,7 @@
"private": true,
"license": "Elastic-License",
"scripts": {
- "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js & node ../../../scripts/eslint ../../legacy/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix",
+ "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js && node ../../../scripts/eslint ../../legacy/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix",
"build-graphql-types": "node scripts/generate_types_from_graphql.js",
"cypress:open": "cypress open --config-file ./cypress/cypress.json",
"cypress:run": "cypress run --spec ./cypress/integration/**/*.spec.ts --config-file ./cypress/cypress.json --reporter ../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; ../../node_modules/.bin/mochawesome-merge --reportDir ../../../target/kibana-siem/cypress/results > ../../../target/kibana-siem/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../target/kibana-siem/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-siem/cypress/results/*.xml ../../../target/junit/ && exit $status;",
diff --git a/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js b/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js
index 478463b1a80640..145d9715970c87 100644
--- a/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js
+++ b/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js
@@ -123,7 +123,7 @@ async function main() {
.replace(/}"/g, '}')
.replace(/"{/g, '{')};
- export const techniques = ${JSON.stringify(techniques, null, 2)};
+ export const technique = ${JSON.stringify(techniques, null, 2)};
export const techniquesOptions: MitreTechniquesOptions[] =
${JSON.stringify(getTechniquesOptions(techniques), null, 2)
diff --git a/x-pack/plugins/siem/server/client/client.test.ts b/x-pack/plugins/siem/server/client/client.test.ts
index 94ff2149b8c64d..c0ae15cb73f4e6 100644
--- a/x-pack/plugins/siem/server/client/client.test.ts
+++ b/x-pack/plugins/siem/server/client/client.test.ts
@@ -9,7 +9,7 @@ import { createMockConfig } from '../lib/detection_engine/routes/__mocks__';
import { SiemClient } from './client';
describe('SiemClient', () => {
- describe('#signalsIndex', () => {
+ describe('#getSignalsIndex', () => {
it('returns the index scoped to the specified spaceId', () => {
const mockConfig = {
...createMockConfig(),
@@ -18,7 +18,7 @@ describe('SiemClient', () => {
const spaceId = 'fooSpace';
const client = new SiemClient(spaceId, mockConfig);
- expect(client.signalsIndex).toEqual('mockSignalsIndex-fooSpace');
+ expect(client.getSignalsIndex()).toEqual('mockSignalsIndex-fooSpace');
});
});
});
diff --git a/x-pack/plugins/siem/server/client/client.ts b/x-pack/plugins/siem/server/client/client.ts
index 6cb0d4cfade77e..5780bb4173f792 100644
--- a/x-pack/plugins/siem/server/client/client.ts
+++ b/x-pack/plugins/siem/server/client/client.ts
@@ -4,14 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ConfigType } from '..';
+import { ConfigType } from '../config';
export class SiemClient {
- public readonly signalsIndex: string;
+ private readonly signalsIndex: string;
constructor(private spaceId: string, private config: ConfigType) {
const configuredSignalsIndex = this.config.signalsIndex;
this.signalsIndex = `${configuredSignalsIndex}-${this.spaceId}`;
}
+
+ public getSignalsIndex = (): string => this.signalsIndex;
}
diff --git a/x-pack/plugins/siem/server/client/factory.ts b/x-pack/plugins/siem/server/client/factory.ts
index d3d6b84e5b090a..69db4d7eed98f3 100644
--- a/x-pack/plugins/siem/server/client/factory.ts
+++ b/x-pack/plugins/siem/server/client/factory.ts
@@ -6,7 +6,7 @@
import { KibanaRequest } from '../../../../../src/core/server';
import { SiemClient } from './client';
-import { ConfigType } from '..';
+import { ConfigType } from '../config';
interface SetupDependencies {
getSpaceId?: (request: KibanaRequest) => string | undefined;
diff --git a/x-pack/plugins/siem/server/index.ts b/x-pack/plugins/siem/server/index.ts
index 83e2f900a3b90e..e9cd78589fac91 100644
--- a/x-pack/plugins/siem/server/index.ts
+++ b/x-pack/plugins/siem/server/index.ts
@@ -5,7 +5,7 @@
*/
import { PluginInitializerContext } from '../../../../src/core/server';
-import { Plugin } from './plugin';
+import { Plugin, PluginSetup, PluginStart } from './plugin';
import { configSchema, ConfigType } from './config';
export const plugin = (context: PluginInitializerContext) => {
@@ -14,4 +14,4 @@ export const plugin = (context: PluginInitializerContext) => {
export const config = { schema: configSchema };
-export { ConfigType };
+export { ConfigType, Plugin, PluginSetup, PluginStart };
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts
index 10efdb518f7b7e..f3b4068f6dd2d4 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts
@@ -13,6 +13,7 @@ import {
import { alertsClientMock } from '../../../../../../alerting/server/mocks';
import { actionsClientMock } from '../../../../../../actions/server/mocks';
import { licensingMock } from '../../../../../../licensing/server/mocks';
+import { siemMock } from '../../../../mocks';
const createMockClients = () => ({
actionsClient: actionsClientMock.create(),
@@ -20,7 +21,7 @@ const createMockClients = () => ({
clusterClient: elasticsearchServiceMock.createScopedClusterClient(),
licensing: { license: licensingMock.createLicenseMock() },
savedObjectsClient: savedObjectsClientMock.create(),
- siemClient: { signalsIndex: 'mockSignalsIndex' },
+ siemClient: siemMock.createClient(),
});
const createRequestContextMock = (
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts
index cb48e352288586..20b8ad29d27155 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts
@@ -37,7 +37,7 @@ export const createIndexRoute = (router: IRouter) => {
return siemResponse.error({ statusCode: 404 });
}
- const index = siemClient.signalsIndex;
+ const index = siemClient.getSignalsIndex();
const indexExists = await getIndexExists(callCluster, index);
if (indexExists) {
return siemResponse.error({
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts
index 5eff38b778492e..79cf4851f9ab81 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts
@@ -45,7 +45,7 @@ export const deleteIndexRoute = (router: IRouter) => {
}
const callCluster = clusterClient.callAsCurrentUser;
- const index = siemClient.signalsIndex;
+ const index = siemClient.getSignalsIndex();
const indexExists = await getIndexExists(callCluster, index);
if (!indexExists) {
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts
index 8ff8d7461ecd11..2b418892f0f392 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts
@@ -29,7 +29,7 @@ export const readIndexRoute = (router: IRouter) => {
return siemResponse.error({ statusCode: 404 });
}
- const index = siemClient.signalsIndex;
+ const index = siemClient.getSignalsIndex();
const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, index);
if (indexExists) {
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts
index 7dbbe837e656d9..e3c41c555f2972 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts
@@ -36,7 +36,7 @@ export const readPrivilegesRoute = (
return siemResponse.error({ statusCode: 404 });
}
- const index = siemClient.signalsIndex;
+ const index = siemClient.getSignalsIndex();
const clusterPrivileges = await readPrivileges(clusterClient.callAsCurrentUser, index);
const privileges = merge(clusterPrivileges, {
is_authenticated: security?.authc.isAuthenticated(request) ?? false,
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts
index bfc8c9c54b2c04..3c6adce45f959f 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts
@@ -49,7 +49,7 @@ export const addPrepackedRulesRoute = (router: IRouter) => {
const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules);
const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules);
- const { signalsIndex } = siemClient;
+ const signalsIndex = siemClient.getSignalsIndex();
if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) {
const signalsIndexExists = await getIndexExists(
clusterClient.callAsCurrentUser,
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts
index 2d7ddb79e5af55..133c98a6af7b3e 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts
@@ -92,7 +92,7 @@ export const createRulesBulkRoute = (router: IRouter) => {
try {
validateLicenseForRuleType({ license: context.licensing.license, ruleType: type });
- const finalIndex = outputIndex ?? siemClient.signalsIndex;
+ const finalIndex = outputIndex ?? siemClient.getSignalsIndex();
const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex);
if (!indexExists) {
return createBulkErrorObject({
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts
index 1f0896686aca05..9f1cddb2051c9b 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts
@@ -9,10 +9,8 @@ import uuid from 'uuid';
import { IRouter } from '../../../../../../../../src/core/server';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
import { createRules } from '../../rules/create_rules';
-import { IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types';
import { readRules } from '../../rules/read_rules';
import { RuleAlertParamsRest } from '../../types';
-import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
import { transformValidate } from './validate';
import { getIndexExists } from '../../index/get_index_exists';
import { createRulesSchema } from '../schemas/create_rules_schema';
@@ -23,6 +21,7 @@ import {
validateLicenseForRuleType,
} from '../utils';
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
+import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
export const createRulesRoute = (router: IRouter): void => {
router.post(
@@ -82,7 +81,7 @@ export const createRulesRoute = (router: IRouter): void => {
return siemResponse.error({ statusCode: 404 });
}
- const finalIndex = outputIndex ?? siemClient.signalsIndex;
+ const finalIndex = outputIndex ?? siemClient.getSignalsIndex();
const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex);
if (!indexExists) {
return siemResponse.error({
@@ -145,10 +144,7 @@ export const createRulesRoute = (router: IRouter): void => {
name,
});
- const ruleStatuses = await savedObjectsClient.find<
- IRuleSavedAttributesSavedObjectAttributes
- >({
- type: ruleStatusSavedObjectType,
+ const ruleStatuses = await ruleStatusSavedObjectsClientFactory(savedObjectsClient).find({
perPage: 1,
sortField: 'statusDate',
sortOrder: 'desc',
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts
index 38748e287ab451..b35ba27ef35619 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts
@@ -11,14 +11,11 @@ import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema';
import { getIdBulkError } from './utils';
import { transformValidateBulkError, validate } from './validate';
import { transformBulkError, buildRouteValidation, buildSiemResponse } from '../utils';
-import {
- IRuleSavedAttributesSavedObjectAttributes,
- DeleteRulesRequestParams,
-} from '../../rules/types';
+import { DeleteRulesRequestParams } from '../../rules/types';
import { deleteRules } from '../../rules/delete_rules';
import { deleteNotifications } from '../../notifications/delete_notifications';
-import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
import { deleteRuleActionsSavedObject } from '../../rule_actions/delete_rule_actions_saved_object';
+import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
type Config = RouteConfig;
type Handler = RequestHandler;
@@ -44,6 +41,8 @@ export const deleteRulesBulkRoute = (router: IRouter) => {
return siemResponse.error({ statusCode: 404 });
}
+ const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
+
const rules = await Promise.all(
request.body.map(async payloadRule => {
const { id, rule_id: ruleId } = payloadRule;
@@ -61,17 +60,12 @@ export const deleteRulesBulkRoute = (router: IRouter) => {
ruleAlertId: rule.id,
savedObjectsClient,
});
- const ruleStatuses = await savedObjectsClient.find<
- IRuleSavedAttributesSavedObjectAttributes
- >({
- type: ruleStatusSavedObjectType,
+ const ruleStatuses = await ruleStatusClient.find({
perPage: 6,
search: rule.id,
searchFields: ['alertId'],
});
- ruleStatuses.saved_objects.forEach(async obj =>
- savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id)
- );
+ ruleStatuses.saved_objects.forEach(async obj => ruleStatusClient.delete(obj.id));
return transformValidateBulkError(idOrRuleIdOrUnknown, rule, undefined, ruleStatuses);
} else {
return getIdBulkError({ id, ruleId });
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts
index 098d556741fed5..2288633ee8d2e0 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts
@@ -11,13 +11,10 @@ import { queryRulesSchema } from '../schemas/query_rules_schema';
import { getIdError } from './utils';
import { transformValidate } from './validate';
import { buildRouteValidation, transformError, buildSiemResponse } from '../utils';
-import {
- DeleteRuleRequestParams,
- IRuleSavedAttributesSavedObjectAttributes,
-} from '../../rules/types';
-import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
+import { DeleteRuleRequestParams } from '../../rules/types';
import { deleteNotifications } from '../../notifications/delete_notifications';
import { deleteRuleActionsSavedObject } from '../../rule_actions/delete_rule_actions_saved_object';
+import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
export const deleteRulesRoute = (router: IRouter) => {
router.delete(
@@ -44,6 +41,7 @@ export const deleteRulesRoute = (router: IRouter) => {
return siemResponse.error({ statusCode: 404 });
}
+ const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
const rule = await deleteRules({
actionsClient,
alertsClient,
@@ -56,17 +54,12 @@ export const deleteRulesRoute = (router: IRouter) => {
ruleAlertId: rule.id,
savedObjectsClient,
});
- const ruleStatuses = await savedObjectsClient.find<
- IRuleSavedAttributesSavedObjectAttributes
- >({
- type: ruleStatusSavedObjectType,
+ const ruleStatuses = await ruleStatusClient.find({
perPage: 6,
search: rule.id,
searchFields: ['alertId'],
});
- ruleStatuses.saved_objects.forEach(async obj =>
- savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id)
- );
+ ruleStatuses.saved_objects.forEach(async obj => ruleStatusClient.delete(obj.id));
const [validated, errors] = transformValidate(
rule,
undefined,
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts
index 8433b74adf3100..bc4568dd0a40b9 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts
@@ -6,7 +6,7 @@
import { IRouter } from '../../../../../../../../src/core/server';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
-import { ConfigType } from '../../../..';
+import { ConfigType } from '../../../../config';
import { ExportRulesRequestParams } from '../../rules/types';
import { getNonPackagedRulesCount } from '../../rules/get_existing_prepackaged_rules';
import { exportRulesSchema, exportRulesQuerySchema } from '../schemas/export_rules_schema';
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts
index 9661fac81497cb..f293b9e64a316a 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts
@@ -7,15 +7,12 @@
import { IRouter } from '../../../../../../../../src/core/server';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
import { findRules } from '../../rules/find_rules';
-import {
- FindRulesRequestParams,
- IRuleSavedAttributesSavedObjectAttributes,
-} from '../../rules/types';
+import { FindRulesRequestParams } from '../../rules/types';
import { findRulesSchema } from '../schemas/find_rules_schema';
import { transformValidateFindAlerts } from './validate';
import { buildRouteValidation, transformError, buildSiemResponse } from '../utils';
-import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object';
+import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
export const findRulesRoute = (router: IRouter) => {
router.get(
@@ -40,6 +37,7 @@ export const findRulesRoute = (router: IRouter) => {
return siemResponse.error({ statusCode: 404 });
}
+ const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
const rules = await findRules({
alertsClient,
perPage: query.per_page,
@@ -50,10 +48,7 @@ export const findRulesRoute = (router: IRouter) => {
});
const ruleStatuses = await Promise.all(
rules.data.map(async rule => {
- const results = await savedObjectsClient.find<
- IRuleSavedAttributesSavedObjectAttributes
- >({
- type: ruleStatusSavedObjectType,
+ const results = await ruleStatusClient.find({
perPage: 1,
sortField: 'statusDate',
sortOrder: 'desc',
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts
index 6b54a25a1b1c47..8e35fecf6a6523 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts
@@ -9,17 +9,16 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
import { findRulesStatusesSchema } from '../schemas/find_rules_statuses_schema';
import {
FindRulesStatusesRequestParams,
- IRuleSavedAttributesSavedObjectAttributes,
RuleStatusResponse,
IRuleStatusAttributes,
} from '../../rules/types';
-import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
import {
buildRouteValidation,
transformError,
convertToSnakeCase,
buildSiemResponse,
} from '../utils';
+import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
export const findRulesStatusesRoute = (router: IRouter) => {
router.post(
@@ -50,12 +49,10 @@ export const findRulesStatusesRoute = (router: IRouter) => {
}
*/
try {
+ const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
const statuses = await body.ids.reduce>(
async (acc, id) => {
- const lastFiveErrorsForId = await savedObjectsClient.find<
- IRuleSavedAttributesSavedObjectAttributes
- >({
- type: ruleStatusSavedObjectType,
+ const lastFiveErrorsForId = await ruleStatusClient.find({
perPage: 6,
sortField: 'statusDate',
sortOrder: 'desc',
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts
index 8c052cfdf4024b..1233e01a677626 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts
@@ -126,6 +126,7 @@ describe('import_rules_route', () => {
});
test('returns an error if the index does not exist', async () => {
+ clients.siemClient.getSignalsIndex.mockReturnValue('mockSignalsIndex');
clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex());
const response = await server.inject(request, context);
expect(response.status).toEqual(200);
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts
index 527fab786910fc..202252da293ee1 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts
@@ -10,7 +10,7 @@ import { extname } from 'path';
import { IRouter } from '../../../../../../../../src/core/server';
import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils/streams';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
-import { ConfigType } from '../../../..';
+import { ConfigType } from '../../../../config';
import { createRules } from '../../rules/create_rules';
import { ImportRulesRequestParams } from '../../rules/types';
import { readRules } from '../../rules/read_rules';
@@ -147,7 +147,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType) => {
ruleType: type,
});
- const signalsIndex = siemClient.signalsIndex;
+ const signalsIndex = siemClient.getSignalsIndex();
const indexExists = await getIndexExists(
clusterClient.callAsCurrentUser,
signalsIndex
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts
index e4236f4632dcd0..534253db65d787 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts
@@ -6,10 +6,7 @@
import { IRouter } from '../../../../../../../../src/core/server';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
-import {
- IRuleSavedAttributesSavedObjectAttributes,
- PatchRuleAlertParamsRest,
-} from '../../rules/types';
+import { PatchRuleAlertParamsRest } from '../../rules/types';
import {
transformBulkError,
buildRouteValidation,
@@ -21,8 +18,8 @@ import { transformValidateBulkError, validate } from './validate';
import { patchRulesBulkSchema } from '../schemas/patch_rules_bulk_schema';
import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema';
import { patchRules } from '../../rules/patch_rules';
-import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
+import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
export const patchRulesBulkRoute = (router: IRouter) => {
router.patch(
@@ -46,6 +43,7 @@ export const patchRulesBulkRoute = (router: IRouter) => {
return siemResponse.error({ statusCode: 404 });
}
+ const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
const rules = await Promise.all(
request.body.map(async payloadRule => {
const {
@@ -131,10 +129,7 @@ export const patchRulesBulkRoute = (router: IRouter) => {
throttle,
name: rule.name,
});
- const ruleStatuses = await savedObjectsClient.find<
- IRuleSavedAttributesSavedObjectAttributes
- >({
- type: ruleStatusSavedObjectType,
+ const ruleStatuses = await ruleStatusClient.find({
perPage: 1,
sortField: 'statusDate',
sortOrder: 'desc',
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts
index 23469144e11f8b..f7932cb016ba7b 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts
@@ -7,10 +7,7 @@
import { IRouter } from '../../../../../../../../src/core/server';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
import { patchRules } from '../../rules/patch_rules';
-import {
- PatchRuleAlertParamsRest,
- IRuleSavedAttributesSavedObjectAttributes,
-} from '../../rules/types';
+import { PatchRuleAlertParamsRest } from '../../rules/types';
import { patchRulesSchema } from '../schemas/patch_rules_schema';
import {
buildRouteValidation,
@@ -20,8 +17,8 @@ import {
} from '../utils';
import { getIdError } from './utils';
import { transformValidate } from './validate';
-import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
+import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
export const patchRulesRoute = (router: IRouter) => {
router.patch(
@@ -83,6 +80,7 @@ export const patchRulesRoute = (router: IRouter) => {
return siemResponse.error({ statusCode: 404 });
}
+ const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
const rule = await patchRules({
actionsClient,
alertsClient,
@@ -127,10 +125,7 @@ export const patchRulesRoute = (router: IRouter) => {
throttle,
name: rule.name,
});
- const ruleStatuses = await savedObjectsClient.find<
- IRuleSavedAttributesSavedObjectAttributes
- >({
- type: ruleStatusSavedObjectType,
+ const ruleStatuses = await ruleStatusClient.find({
perPage: 1,
sortField: 'statusDate',
sortOrder: 'desc',
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts
index 4d23e0217f2e8b..cedd7ccd1a411b 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts
@@ -11,12 +11,9 @@ import { transformValidate } from './validate';
import { buildRouteValidation, transformError, buildSiemResponse } from '../utils';
import { readRules } from '../../rules/read_rules';
import { queryRulesSchema } from '../schemas/query_rules_schema';
-import {
- ReadRuleRequestParams,
- IRuleSavedAttributesSavedObjectAttributes,
-} from '../../rules/types';
-import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
+import { ReadRuleRequestParams } from '../../rules/types';
import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object';
+import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
export const readRulesRoute = (router: IRouter) => {
router.get(
@@ -41,6 +38,7 @@ export const readRulesRoute = (router: IRouter) => {
return siemResponse.error({ statusCode: 404 });
}
+ const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
const rule = await readRules({
alertsClient,
id,
@@ -51,10 +49,7 @@ export const readRulesRoute = (router: IRouter) => {
savedObjectsClient,
ruleAlertId: rule.id,
});
- const ruleStatuses = await savedObjectsClient.find<
- IRuleSavedAttributesSavedObjectAttributes
- >({
- type: ruleStatusSavedObjectType,
+ const ruleStatuses = await ruleStatusClient.find({
perPage: 1,
sortField: 'statusDate',
sortOrder: 'desc',
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts
index 6db91d74294fc6..f929f2fb3f6495 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts
@@ -6,10 +6,7 @@
import { IRouter } from '../../../../../../../../src/core/server';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
-import {
- IRuleSavedAttributesSavedObjectAttributes,
- UpdateRuleAlertParamsRest,
-} from '../../rules/types';
+import { UpdateRuleAlertParamsRest } from '../../rules/types';
import { getIdBulkError } from './utils';
import { transformValidateBulkError, validate } from './validate';
import {
@@ -19,10 +16,10 @@ import {
validateLicenseForRuleType,
} from '../utils';
import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema';
-import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
import { updateRules } from '../../rules/update_rules';
import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema';
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
+import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
export const updateRulesBulkRoute = (router: IRouter) => {
router.put(
@@ -47,6 +44,7 @@ export const updateRulesBulkRoute = (router: IRouter) => {
return siemResponse.error({ statusCode: 404 });
}
+ const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
const rules = await Promise.all(
request.body.map(async payloadRule => {
const {
@@ -83,7 +81,7 @@ export const updateRulesBulkRoute = (router: IRouter) => {
version,
exceptions_list,
} = payloadRule;
- const finalIndex = outputIndex ?? siemClient.signalsIndex;
+ const finalIndex = outputIndex ?? siemClient.getSignalsIndex();
const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)';
try {
validateLicenseForRuleType({ license: context.licensing.license, ruleType: type });
@@ -134,10 +132,7 @@ export const updateRulesBulkRoute = (router: IRouter) => {
throttle,
name,
});
- const ruleStatuses = await savedObjectsClient.find<
- IRuleSavedAttributesSavedObjectAttributes
- >({
- type: ruleStatusSavedObjectType,
+ const ruleStatuses = await ruleStatusClient.find({
perPage: 1,
sortField: 'statusDate',
sortOrder: 'desc',
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts
index 7dbbe5a22ab46a..dedc2c914410a9 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts
@@ -6,10 +6,7 @@
import { IRouter } from '../../../../../../../../src/core/server';
import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants';
-import {
- UpdateRuleAlertParamsRest,
- IRuleSavedAttributesSavedObjectAttributes,
-} from '../../rules/types';
+import { UpdateRuleAlertParamsRest } from '../../rules/types';
import { updateRulesSchema } from '../schemas/update_rules_schema';
import {
buildRouteValidation,
@@ -19,9 +16,9 @@ import {
} from '../utils';
import { getIdError } from './utils';
import { transformValidate } from './validate';
-import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
import { updateRules } from '../../rules/update_rules';
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
+import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client';
export const updateRulesRoute = (router: IRouter) => {
router.put(
@@ -78,12 +75,13 @@ export const updateRulesRoute = (router: IRouter) => {
const actionsClient = context.actions?.getActionsClient();
const savedObjectsClient = context.core.savedObjects.client;
const siemClient = context.siem?.getSiemClient();
+ const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
if (!siemClient || !actionsClient || !alertsClient) {
return siemResponse.error({ statusCode: 404 });
}
- const finalIndex = outputIndex ?? siemClient.signalsIndex;
+ const finalIndex = outputIndex ?? siemClient.getSignalsIndex();
const rule = await updateRules({
alertsClient,
actionsClient,
@@ -131,10 +129,7 @@ export const updateRulesRoute = (router: IRouter) => {
throttle,
name,
});
- const ruleStatuses = await savedObjectsClient.find<
- IRuleSavedAttributesSavedObjectAttributes
- >({
- type: ruleStatusSavedObjectType,
+ const ruleStatuses = await ruleStatusClient.find({
perPage: 1,
sortField: 'statusDate',
sortOrder: 'desc',
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts
index c71761fcc39dbd..bcb70b6b4f0dd8 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts
@@ -44,7 +44,7 @@ export const setSignalsStatusRoute = (router: IRouter) => {
}
try {
const result = await clusterClient.callAsCurrentUser('updateByQuery', {
- index: siemClient.signalsIndex,
+ index: siemClient.getSignalsIndex(),
body: {
script: {
source: `ctx._source.signal.status = '${status}'`,
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts
index fd02b3371ed38e..41896c725b903d 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts
@@ -29,7 +29,7 @@ export const querySignalsRoute = (router: IRouter) => {
try {
const result = await clusterClient.callAsCurrentUser('search', {
- index: siemClient.signalsIndex,
+ index: siemClient.getSignalsIndex(),
body: { query, aggs, _source, track_total_hits, size },
ignoreUnavailable: true,
});
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts
index f54f43c41ef6ee..d50c339c95266c 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts
@@ -4,37 +4,45 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { SavedObjectsType } from '../../../../../../../src/core/server';
+
export const ruleActionsSavedObjectType = 'siem-detection-engine-rule-actions';
export const ruleActionsSavedObjectMappings = {
- [ruleActionsSavedObjectType]: {
- properties: {
- alertThrottle: {
- type: 'keyword',
- },
- ruleAlertId: {
- type: 'keyword',
- },
- ruleThrottle: {
- type: 'keyword',
- },
- actions: {
- properties: {
- group: {
- type: 'keyword',
- },
- id: {
- type: 'keyword',
- },
- action_type_id: {
- type: 'keyword',
- },
- params: {
- dynamic: true,
- properties: {},
- },
+ properties: {
+ alertThrottle: {
+ type: 'keyword',
+ },
+ ruleAlertId: {
+ type: 'keyword',
+ },
+ ruleThrottle: {
+ type: 'keyword',
+ },
+ actions: {
+ properties: {
+ group: {
+ type: 'keyword',
+ },
+ id: {
+ type: 'keyword',
+ },
+ action_type_id: {
+ type: 'keyword',
+ },
+ params: {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ dynamic: true as any,
+ properties: {},
},
},
},
},
};
+
+export const type: SavedObjectsType = {
+ name: ruleActionsSavedObjectType,
+ hidden: false,
+ namespaceType: 'single',
+ mappings: ruleActionsSavedObjectMappings,
+};
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts
index c23f539b581606..85b13ed9cf4ed8 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts
@@ -7,10 +7,10 @@
import { defaults } from 'lodash/fp';
import { PartialAlert } from '../../../../../alerting/server';
import { readRules } from './read_rules';
-import { PatchRuleParams, IRuleSavedAttributesSavedObjectAttributes } from './types';
+import { PatchRuleParams } from './types';
import { addTags } from './add_tags';
-import { ruleStatusSavedObjectType } from './saved_object_mappings';
import { calculateVersion, calculateName, calculateInterval } from './utils';
+import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client';
export const patchRules = async ({
alertsClient,
@@ -134,22 +134,22 @@ export const patchRules = async ({
await alertsClient.disable({ id: rule.id });
} else if (!rule.enabled && enabled === true) {
await alertsClient.enable({ id: rule.id });
- const ruleCurrentStatus = savedObjectsClient
- ? await savedObjectsClient.find({
- type: ruleStatusSavedObjectType,
- perPage: 1,
- sortField: 'statusDate',
- sortOrder: 'desc',
- search: rule.id,
- searchFields: ['alertId'],
- })
- : null;
+
+ const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
+ const ruleCurrentStatus = await ruleStatusClient.find({
+ perPage: 1,
+ sortField: 'statusDate',
+ sortOrder: 'desc',
+ search: rule.id,
+ searchFields: ['alertId'],
+ });
+
// set current status for this rule to be 'going to run'
if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) {
const currentStatusToDisable = ruleCurrentStatus.saved_objects[0];
- currentStatusToDisable.attributes.status = 'going to run';
- await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, {
+ await ruleStatusClient.update(currentStatusToDisable.id, {
...currentStatusToDisable.attributes,
+ status: 'going to run',
});
}
} else {
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts
index 1d91def5fa6cc9..2dcc90240ad407 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts
@@ -4,44 +4,51 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { SavedObjectsType } from '../../../../../../../src/core/server';
+
export const ruleStatusSavedObjectType = 'siem-detection-engine-rule-status';
export const ruleStatusSavedObjectMappings = {
- [ruleStatusSavedObjectType]: {
- properties: {
- alertId: {
- type: 'keyword',
- },
- status: {
- type: 'keyword',
- },
- statusDate: {
- type: 'date',
- },
- lastFailureAt: {
- type: 'date',
- },
- lastSuccessAt: {
- type: 'date',
- },
- lastFailureMessage: {
- type: 'text',
- },
- lastSuccessMessage: {
- type: 'text',
- },
- lastLookBackDate: {
- type: 'date',
- },
- gap: {
- type: 'text',
- },
- bulkCreateTimeDurations: {
- type: 'float',
- },
- searchAfterTimeDurations: {
- type: 'float',
- },
+ properties: {
+ alertId: {
+ type: 'keyword',
+ },
+ status: {
+ type: 'keyword',
+ },
+ statusDate: {
+ type: 'date',
+ },
+ lastFailureAt: {
+ type: 'date',
+ },
+ lastSuccessAt: {
+ type: 'date',
+ },
+ lastFailureMessage: {
+ type: 'text',
+ },
+ lastSuccessMessage: {
+ type: 'text',
+ },
+ lastLookBackDate: {
+ type: 'date',
+ },
+ gap: {
+ type: 'text',
+ },
+ bulkCreateTimeDurations: {
+ type: 'float',
+ },
+ searchAfterTimeDurations: {
+ type: 'float',
},
},
};
+
+export const type: SavedObjectsType = {
+ name: ruleStatusSavedObjectType,
+ hidden: false,
+ namespaceType: 'single',
+ mappings: ruleStatusSavedObjectMappings,
+};
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts
index 7ddbbd76b06618..29c2cfdf91076a 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts
@@ -7,11 +7,11 @@
import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions';
import { PartialAlert } from '../../../../../alerting/server';
import { readRules } from './read_rules';
-import { IRuleSavedAttributesSavedObjectAttributes, UpdateRuleParams } from './types';
+import { UpdateRuleParams } from './types';
import { addTags } from './add_tags';
-import { ruleStatusSavedObjectType } from './saved_object_mappings';
import { calculateVersion } from './utils';
import { hasListsFeature } from '../feature_flags';
+import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client';
export const updateRules = async ({
alertsClient,
@@ -129,22 +129,22 @@ export const updateRules = async ({
await alertsClient.disable({ id: rule.id });
} else if (!rule.enabled && enabled === true) {
await alertsClient.enable({ id: rule.id });
- const ruleCurrentStatus = savedObjectsClient
- ? await savedObjectsClient.find({
- type: ruleStatusSavedObjectType,
- perPage: 1,
- sortField: 'statusDate',
- sortOrder: 'desc',
- search: rule.id,
- searchFields: ['alertId'],
- })
- : null;
+
+ const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient);
+ const ruleCurrentStatus = await ruleStatusClient.find({
+ perPage: 1,
+ sortField: 'statusDate',
+ sortOrder: 'desc',
+ search: rule.id,
+ searchFields: ['alertId'],
+ });
+
// set current status for this rule to be 'going to run'
if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) {
const currentStatusToDisable = ruleCurrentStatus.saved_objects[0];
- currentStatusToDisable.attributes.status = 'going to run';
- await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, {
+ await ruleStatusClient.update(currentStatusToDisable.id, {
...currentStatusToDisable.attributes,
+ status: 'going to run',
});
}
}
diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts
index 731fffcac1bb0e..251a1e6d118ff1 100644
--- a/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts
+++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts
@@ -13,7 +13,7 @@ import {
import { loggingServiceMock } from '../../../../../../../../src/core/server/mocks';
import { RuleTypeParams, OutputRuleAlertRest } from '../../types';
import { IRuleStatusAttributes } from '../../rules/types';
-import { ruleStatusSavedObjectType } from '../../../../saved_objects';
+import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings';
export const sampleRuleAlertParams = (
maxSignals?: number | undefined,
diff --git a/x-pack/plugins/siem/server/lib/note/saved_object.ts b/x-pack/plugins/siem/server/lib/note/saved_object.ts
index 2b94fd4516786e..3eae30625e4223 100644
--- a/x-pack/plugins/siem/server/lib/note/saved_object.ts
+++ b/x-pack/plugins/siem/server/lib/note/saved_object.ts
@@ -25,9 +25,9 @@ import {
import { FrameworkRequest } from '../framework';
import { SavedNote, NoteSavedObjectRuntimeType, NoteSavedObject } from './types';
import { noteSavedObjectType } from './saved_object_mappings';
-import { timelineSavedObjectType } from '../../saved_objects';
import { pickSavedTimeline } from '../timeline/pick_saved_timeline';
import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline';
+import { timelineSavedObjectType } from '../timeline/saved_object_mappings';
export class Note {
public async deleteNote(request: FrameworkRequest, noteIds: string[]) {
diff --git a/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts
index b001e30e523362..0f079571b868b5 100644
--- a/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts
+++ b/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts
@@ -4,37 +4,39 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings';
-import { SavedNote } from './types';
+import { SavedObjectsType } from '../../../../../../src/core/server';
export const noteSavedObjectType = 'siem-ui-timeline-note';
-export const noteSavedObjectMappings: {
- [noteSavedObjectType]: ElasticsearchMappingOf;
-} = {
- [noteSavedObjectType]: {
- properties: {
- timelineId: {
- type: 'keyword',
- },
- eventId: {
- type: 'keyword',
- },
- note: {
- type: 'text',
- },
- created: {
- type: 'date',
- },
- createdBy: {
- type: 'text',
- },
- updated: {
- type: 'date',
- },
- updatedBy: {
- type: 'text',
- },
+export const noteSavedObjectMappings = {
+ properties: {
+ timelineId: {
+ type: 'keyword',
+ },
+ eventId: {
+ type: 'keyword',
+ },
+ note: {
+ type: 'text',
+ },
+ created: {
+ type: 'date',
+ },
+ createdBy: {
+ type: 'text',
+ },
+ updated: {
+ type: 'date',
+ },
+ updatedBy: {
+ type: 'text',
},
},
};
+
+export const type: SavedObjectsType = {
+ name: noteSavedObjectType,
+ hidden: false,
+ namespaceType: 'single',
+ mappings: noteSavedObjectMappings,
+};
diff --git a/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts b/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts
index 7fc23d86d82186..1e3a481e17106f 100644
--- a/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts
+++ b/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts
@@ -20,9 +20,10 @@ import {
SavedPinnedEvent,
} from './types';
import { PageInfoNote, SortNote, PinnedEvent as PinnedEventResponse } from '../../graphql/types';
-import { pinnedEventSavedObjectType, timelineSavedObjectType } from '../../saved_objects';
import { pickSavedTimeline } from '../timeline/pick_saved_timeline';
import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline';
+import { pinnedEventSavedObjectType } from './saved_object_mappings';
+import { timelineSavedObjectType } from '../timeline/saved_object_mappings';
export class PinnedEvent {
public async deletePinnedEventOnTimeline(request: FrameworkRequest, pinnedEventIds: string[]) {
diff --git a/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts
index 322f585ae8ff28..1a4cd3fce575d6 100644
--- a/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts
+++ b/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts
@@ -4,34 +4,36 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings';
-import { SavedPinnedEvent } from './types';
+import { SavedObjectsType } from '../../../../../../src/core/server';
export const pinnedEventSavedObjectType = 'siem-ui-timeline-pinned-event';
-export const pinnedEventSavedObjectMappings: {
- [pinnedEventSavedObjectType]: ElasticsearchMappingOf;
-} = {
- [pinnedEventSavedObjectType]: {
- properties: {
- timelineId: {
- type: 'keyword',
- },
- eventId: {
- type: 'keyword',
- },
- created: {
- type: 'date',
- },
- createdBy: {
- type: 'text',
- },
- updated: {
- type: 'date',
- },
- updatedBy: {
- type: 'text',
- },
+export const pinnedEventSavedObjectMappings = {
+ properties: {
+ timelineId: {
+ type: 'keyword',
+ },
+ eventId: {
+ type: 'keyword',
+ },
+ created: {
+ type: 'date',
+ },
+ createdBy: {
+ type: 'text',
+ },
+ updated: {
+ type: 'date',
+ },
+ updatedBy: {
+ type: 'text',
},
},
};
+
+export const type: SavedObjectsType = {
+ name: pinnedEventSavedObjectType,
+ hidden: false,
+ namespaceType: 'single',
+ mappings: pinnedEventSavedObjectMappings,
+};
diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts
index c59f6eb6ce3daa..e0eefbf811a565 100644
--- a/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts
+++ b/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts
@@ -8,7 +8,7 @@ import { set as _set } from 'lodash/fp';
import { TIMELINE_EXPORT_URL } from '../../../../common/constants';
import { IRouter } from '../../../../../../../src/core/server';
-import { ConfigType } from '../../..';
+import { ConfigType } from '../../../config';
import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils';
import { getExportTimelineByObjectIds } from './utils/export_timelines';
diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts
index 258ef9faf671bd..9d148abf82cddd 100644
--- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts
+++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts
@@ -32,7 +32,7 @@ import { IRouter } from '../../../../../../../src/core/server';
import { SetupPlugins } from '../../../plugin';
import { ImportTimelinesPayloadSchemaRt } from './schemas/import_timelines_schema';
import { importRulesSchema } from '../../detection_engine/routes/schemas/response/import_rules_schema';
-import { ConfigType } from '../../..';
+import { ConfigType } from '../../../config';
import { Timeline } from '../saved_object';
import { validate } from '../../detection_engine/routes/rules/validate';
diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts
index edd4abe0d76b5f..677891fa16c024 100644
--- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts
+++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts
@@ -4,12 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { set as _set } from 'lodash/fp';
-import {
- noteSavedObjectType,
- pinnedEventSavedObjectType,
- timelineSavedObjectType,
-} from '../../../../saved_objects';
import { NoteSavedObject } from '../../../note/types';
import { PinnedEventSavedObject } from '../../../pinned_event/types';
import { convertSavedObjectToSavedTimeline } from '../../convert_saved_object_to_savedtimeline';
@@ -30,6 +24,9 @@ import {
TimelineSavedObject,
} from '../../types';
import { transformDataToNdjson } from '../../../../utils/read_stream/create_stream_from_ndjson';
+import { pinnedEventSavedObjectType } from '../../../pinned_event/saved_object_mappings';
+import { noteSavedObjectType } from '../../../note/saved_object_mappings';
+import { timelineSavedObjectType } from '../../saved_object_mappings';
export type TimelineSavedObjectsClient = Pick<
SavedObjectsClient,
diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts
index 8fc12fd56a8f60..b956e0f98fcb62 100644
--- a/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts
+++ b/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts
@@ -4,272 +4,274 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings';
-import { SavedTimeline } from './types';
+import { SavedObjectsType } from '../../../../../../src/core/server';
export const timelineSavedObjectType = 'siem-ui-timeline';
-export const timelineSavedObjectMappings: {
- [timelineSavedObjectType]: ElasticsearchMappingOf;
-} = {
- [timelineSavedObjectType]: {
- properties: {
- columns: {
- properties: {
- aggregatable: {
- type: 'boolean',
- },
- category: {
- type: 'keyword',
- },
- columnHeaderType: {
- type: 'keyword',
- },
- description: {
- type: 'text',
- },
- example: {
- type: 'text',
- },
- indexes: {
- type: 'keyword',
- },
- id: {
- type: 'keyword',
- },
- name: {
- type: 'text',
- },
- placeholder: {
- type: 'text',
- },
- searchable: {
- type: 'boolean',
- },
- type: {
- type: 'keyword',
- },
+export const timelineSavedObjectMappings = {
+ properties: {
+ columns: {
+ properties: {
+ aggregatable: {
+ type: 'boolean',
+ },
+ category: {
+ type: 'keyword',
+ },
+ columnHeaderType: {
+ type: 'keyword',
+ },
+ description: {
+ type: 'text',
+ },
+ example: {
+ type: 'text',
+ },
+ indexes: {
+ type: 'keyword',
+ },
+ id: {
+ type: 'keyword',
+ },
+ name: {
+ type: 'text',
+ },
+ placeholder: {
+ type: 'text',
+ },
+ searchable: {
+ type: 'boolean',
+ },
+ type: {
+ type: 'keyword',
},
},
- dataProviders: {
- properties: {
- id: {
- type: 'keyword',
- },
- name: {
- type: 'text',
- },
- enabled: {
- type: 'boolean',
- },
- excluded: {
- type: 'boolean',
- },
- kqlQuery: {
- type: 'text',
- },
- queryMatch: {
- properties: {
- field: {
- type: 'text',
- },
- displayField: {
- type: 'text',
- },
- value: {
- type: 'text',
- },
- displayValue: {
- type: 'text',
- },
- operator: {
- type: 'text',
- },
+ },
+ dataProviders: {
+ properties: {
+ id: {
+ type: 'keyword',
+ },
+ name: {
+ type: 'text',
+ },
+ enabled: {
+ type: 'boolean',
+ },
+ excluded: {
+ type: 'boolean',
+ },
+ kqlQuery: {
+ type: 'text',
+ },
+ queryMatch: {
+ properties: {
+ field: {
+ type: 'text',
+ },
+ displayField: {
+ type: 'text',
+ },
+ value: {
+ type: 'text',
+ },
+ displayValue: {
+ type: 'text',
+ },
+ operator: {
+ type: 'text',
},
},
- and: {
- properties: {
- id: {
- type: 'keyword',
- },
- name: {
- type: 'text',
- },
- enabled: {
- type: 'boolean',
- },
- excluded: {
- type: 'boolean',
- },
- kqlQuery: {
- type: 'text',
- },
- queryMatch: {
- properties: {
- field: {
- type: 'text',
- },
- displayField: {
- type: 'text',
- },
- value: {
- type: 'text',
- },
- displayValue: {
- type: 'text',
- },
- operator: {
- type: 'text',
- },
+ },
+ and: {
+ properties: {
+ id: {
+ type: 'keyword',
+ },
+ name: {
+ type: 'text',
+ },
+ enabled: {
+ type: 'boolean',
+ },
+ excluded: {
+ type: 'boolean',
+ },
+ kqlQuery: {
+ type: 'text',
+ },
+ queryMatch: {
+ properties: {
+ field: {
+ type: 'text',
+ },
+ displayField: {
+ type: 'text',
+ },
+ value: {
+ type: 'text',
+ },
+ displayValue: {
+ type: 'text',
+ },
+ operator: {
+ type: 'text',
},
},
},
},
},
},
- description: {
- type: 'text',
- },
- eventType: {
- type: 'keyword',
- },
- favorite: {
- properties: {
- keySearch: {
- type: 'text',
- },
- fullName: {
- type: 'text',
- },
- userName: {
- type: 'text',
- },
- favoriteDate: {
- type: 'date',
- },
+ },
+ description: {
+ type: 'text',
+ },
+ eventType: {
+ type: 'keyword',
+ },
+ favorite: {
+ properties: {
+ keySearch: {
+ type: 'text',
+ },
+ fullName: {
+ type: 'text',
+ },
+ userName: {
+ type: 'text',
+ },
+ favoriteDate: {
+ type: 'date',
},
},
- filters: {
- properties: {
- meta: {
- properties: {
- alias: {
- type: 'text',
- },
- controlledBy: {
- type: 'text',
- },
- disabled: {
- type: 'boolean',
- },
- field: {
- type: 'text',
- },
- formattedValue: {
- type: 'text',
- },
- index: {
- type: 'keyword',
- },
- key: {
- type: 'keyword',
- },
- negate: {
- type: 'boolean',
- },
- params: {
- type: 'text',
- },
- type: {
- type: 'keyword',
- },
- value: {
- type: 'text',
- },
+ },
+ filters: {
+ properties: {
+ meta: {
+ properties: {
+ alias: {
+ type: 'text',
+ },
+ controlledBy: {
+ type: 'text',
+ },
+ disabled: {
+ type: 'boolean',
+ },
+ field: {
+ type: 'text',
+ },
+ formattedValue: {
+ type: 'text',
+ },
+ index: {
+ type: 'keyword',
+ },
+ key: {
+ type: 'keyword',
+ },
+ negate: {
+ type: 'boolean',
+ },
+ params: {
+ type: 'text',
+ },
+ type: {
+ type: 'keyword',
+ },
+ value: {
+ type: 'text',
},
- },
- exists: {
- type: 'text',
- },
- match_all: {
- type: 'text',
- },
- missing: {
- type: 'text',
- },
- query: {
- type: 'text',
- },
- range: {
- type: 'text',
- },
- script: {
- type: 'text',
},
},
+ exists: {
+ type: 'text',
+ },
+ match_all: {
+ type: 'text',
+ },
+ missing: {
+ type: 'text',
+ },
+ query: {
+ type: 'text',
+ },
+ range: {
+ type: 'text',
+ },
+ script: {
+ type: 'text',
+ },
},
- kqlMode: {
- type: 'keyword',
- },
- kqlQuery: {
- properties: {
- filterQuery: {
- properties: {
- kuery: {
- properties: {
- kind: {
- type: 'keyword',
- },
- expression: {
- type: 'text',
- },
+ },
+ kqlMode: {
+ type: 'keyword',
+ },
+ kqlQuery: {
+ properties: {
+ filterQuery: {
+ properties: {
+ kuery: {
+ properties: {
+ kind: {
+ type: 'keyword',
+ },
+ expression: {
+ type: 'text',
},
},
- serializedQuery: {
- type: 'text',
- },
+ },
+ serializedQuery: {
+ type: 'text',
},
},
},
},
- title: {
- type: 'text',
- },
- dateRange: {
- properties: {
- start: {
- type: 'date',
- },
- end: {
- type: 'date',
- },
+ },
+ title: {
+ type: 'text',
+ },
+ dateRange: {
+ properties: {
+ start: {
+ type: 'date',
},
- },
- savedQueryId: {
- type: 'keyword',
- },
- sort: {
- properties: {
- columnId: {
- type: 'keyword',
- },
- sortDirection: {
- type: 'keyword',
- },
+ end: {
+ type: 'date',
},
},
- created: {
- type: 'date',
- },
- createdBy: {
- type: 'text',
- },
- updated: {
- type: 'date',
- },
- updatedBy: {
- type: 'text',
+ },
+ savedQueryId: {
+ type: 'keyword',
+ },
+ sort: {
+ properties: {
+ columnId: {
+ type: 'keyword',
+ },
+ sortDirection: {
+ type: 'keyword',
+ },
},
},
+ created: {
+ type: 'date',
+ },
+ createdBy: {
+ type: 'text',
+ },
+ updated: {
+ type: 'date',
+ },
+ updatedBy: {
+ type: 'text',
+ },
},
};
+
+export const type: SavedObjectsType = {
+ name: timelineSavedObjectType,
+ hidden: false,
+ namespaceType: 'single',
+ mappings: timelineSavedObjectMappings,
+};
diff --git a/x-pack/plugins/siem/server/lib/types.ts b/x-pack/plugins/siem/server/lib/types.ts
index a74fe8f778ba94..2a897806dc6287 100644
--- a/x-pack/plugins/siem/server/lib/types.ts
+++ b/x-pack/plugins/siem/server/lib/types.ts
@@ -6,7 +6,7 @@
import { AuthenticatedUser } from '../../../security/public';
import { RequestHandlerContext } from '../../../../../src/core/server';
-export { ConfigType as Configuration } from '../';
+export { ConfigType as Configuration } from '../config';
import { Authentications } from './authentications';
import { Events } from './events';
diff --git a/x-pack/plugins/siem/server/mocks.ts b/x-pack/plugins/siem/server/mocks.ts
new file mode 100644
index 00000000000000..44c41be86b6ffe
--- /dev/null
+++ b/x-pack/plugins/siem/server/mocks.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.
+ */
+
+import { SiemClient } from './types';
+
+type SiemClientMock = jest.Mocked;
+const createSiemClientMock = (): SiemClientMock =>
+ (({
+ getSignalsIndex: jest.fn(),
+ } as unknown) as SiemClientMock);
+
+export const siemMock = {
+ createClient: createSiemClientMock,
+};
diff --git a/x-pack/plugins/siem/server/plugin.ts b/x-pack/plugins/siem/server/plugin.ts
index b9ec1c2e92438b..3988fbec05de4f 100644
--- a/x-pack/plugins/siem/server/plugin.ts
+++ b/x-pack/plugins/siem/server/plugin.ts
@@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n';
import {
CoreSetup,
CoreStart,
+ Plugin as IPlugin,
PluginInitializerContext,
Logger,
} from '../../../../src/core/server';
@@ -33,15 +34,10 @@ import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule
import { rulesNotificationAlertType } from './lib/detection_engine/notifications/rules_notification_alert_type';
import { isNotificationAlertExecutor } from './lib/detection_engine/notifications/types';
import { hasListsFeature, listsEnvFeatureFlagName } from './lib/detection_engine/feature_flags';
-import {
- noteSavedObjectType,
- pinnedEventSavedObjectType,
- timelineSavedObjectType,
- ruleStatusSavedObjectType,
- ruleActionsSavedObjectType,
-} from './saved_objects';
+import { initSavedObjects, savedObjectTypes } from './saved_objects';
import { SiemClientFactory } from './client';
import { createConfig$, ConfigType } from './config';
+import { initUiSettings } from './ui_settings';
export { CoreSetup, CoreStart };
@@ -60,7 +56,12 @@ export interface StartPlugins {
alerting: AlertingStart;
}
-export class Plugin {
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface PluginSetup {}
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface PluginStart {}
+
+export class Plugin implements IPlugin {
readonly name = 'siem';
private readonly logger: Logger;
private readonly config$: Observable;
@@ -86,6 +87,9 @@ export class Plugin {
);
}
+ initSavedObjects(core.savedObjects);
+ initUiSettings(core.uiSettings);
+
const router = core.http.createRouter();
core.http.registerRouteHandlerContext(this.name, (context, request, response) => ({
getSiemClient: () => this.siemClientFactory.create(request),
@@ -125,15 +129,11 @@ export class Plugin {
'alert',
'action',
'action_task_params',
- noteSavedObjectType,
- pinnedEventSavedObjectType,
- timelineSavedObjectType,
- ruleStatusSavedObjectType,
- ruleActionsSavedObjectType,
'cases',
'cases-comments',
'cases-configure',
'cases-user-actions',
+ ...savedObjectTypes,
],
read: ['config'],
},
@@ -156,15 +156,11 @@ export class Plugin {
all: ['alert', 'action', 'action_task_params'],
read: [
'config',
- noteSavedObjectType,
- pinnedEventSavedObjectType,
- timelineSavedObjectType,
- ruleStatusSavedObjectType,
- ruleActionsSavedObjectType,
'cases',
'cases-comments',
'cases-configure',
'cases-user-actions',
+ ...savedObjectTypes,
],
},
ui: [
@@ -201,7 +197,11 @@ export class Plugin {
const libs = compose(core, plugins, this.context.env.mode.prod);
initServer(libs);
+
+ return {};
}
- public start(core: CoreStart, plugins: StartPlugins) {}
+ public start(core: CoreStart, plugins: StartPlugins) {
+ return {};
+ }
}
diff --git a/x-pack/plugins/siem/server/routes/index.ts b/x-pack/plugins/siem/server/routes/index.ts
index 64b232a2686b87..1c03823e85fd7a 100644
--- a/x-pack/plugins/siem/server/routes/index.ts
+++ b/x-pack/plugins/siem/server/routes/index.ts
@@ -31,7 +31,7 @@ import { getPrepackagedRulesStatusRoute } from '../lib/detection_engine/routes/r
import { importTimelinesRoute } from '../lib/timeline/routes/import_timelines_route';
import { exportTimelinesRoute } from '../lib/timeline/routes/export_timelines_route';
import { SetupPlugins } from '../plugin';
-import { ConfigType } from '..';
+import { ConfigType } from '../config';
export const initRoutes = (
router: IRouter,
diff --git a/x-pack/plugins/siem/server/saved_objects.ts b/x-pack/plugins/siem/server/saved_objects.ts
index 7b097eefedb467..66a470099d6499 100644
--- a/x-pack/plugins/siem/server/saved_objects.ts
+++ b/x-pack/plugins/siem/server/saved_objects.ts
@@ -4,35 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { noteSavedObjectType, noteSavedObjectMappings } from './lib/note/saved_object_mappings';
-import {
- pinnedEventSavedObjectType,
- pinnedEventSavedObjectMappings,
-} from './lib/pinned_event/saved_object_mappings';
-import {
- timelineSavedObjectType,
- timelineSavedObjectMappings,
-} from './lib/timeline/saved_object_mappings';
-import {
- ruleStatusSavedObjectMappings,
- ruleStatusSavedObjectType,
-} from './lib/detection_engine/rules/saved_object_mappings';
-import {
- ruleActionsSavedObjectMappings,
- ruleActionsSavedObjectType,
-} from './lib/detection_engine/rule_actions/saved_object_mappings';
+import { CoreSetup } from '../../../../src/core/server';
-export {
- noteSavedObjectType,
- pinnedEventSavedObjectType,
- ruleStatusSavedObjectType,
- ruleActionsSavedObjectType,
- timelineSavedObjectType,
-};
-export const savedObjectMappings = {
- ...timelineSavedObjectMappings,
- ...noteSavedObjectMappings,
- ...pinnedEventSavedObjectMappings,
- ...ruleStatusSavedObjectMappings,
- ...ruleActionsSavedObjectMappings,
+import { type as noteType } from './lib/note/saved_object_mappings';
+import { type as pinnedEventType } from './lib/pinned_event/saved_object_mappings';
+import { type as timelineType } from './lib/timeline/saved_object_mappings';
+import { type as ruleStatusType } from './lib/detection_engine/rules/saved_object_mappings';
+import { type as ruleActionsType } from './lib/detection_engine/rule_actions/saved_object_mappings';
+
+const types = [noteType, pinnedEventType, ruleActionsType, ruleStatusType, timelineType];
+
+export const savedObjectTypes = types.map(type => type.name);
+
+export const initSavedObjects = (savedObjects: CoreSetup['savedObjects']) => {
+ types.forEach(type => savedObjects.registerType(type));
};
diff --git a/x-pack/plugins/siem/server/ui_settings.ts b/x-pack/plugins/siem/server/ui_settings.ts
new file mode 100644
index 00000000000000..26b7fd72571afe
--- /dev/null
+++ b/x-pack/plugins/siem/server/ui_settings.ts
@@ -0,0 +1,141 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { schema } from '@kbn/config-schema';
+
+import { CoreSetup } from '../../../../src/core/server';
+import {
+ DEFAULT_INDEX_KEY,
+ DEFAULT_INDEX_PATTERN,
+ DEFAULT_ANOMALY_SCORE,
+ DEFAULT_SIEM_TIME_RANGE,
+ DEFAULT_SIEM_REFRESH_INTERVAL,
+ DEFAULT_INTERVAL_PAUSE,
+ DEFAULT_INTERVAL_VALUE,
+ DEFAULT_FROM,
+ DEFAULT_TO,
+ ENABLE_NEWS_FEED_SETTING,
+ NEWS_FEED_URL_SETTING,
+ NEWS_FEED_URL_SETTING_DEFAULT,
+ IP_REPUTATION_LINKS_SETTING,
+ IP_REPUTATION_LINKS_SETTING_DEFAULT,
+} from '../common/constants';
+
+export const initUiSettings = (uiSettings: CoreSetup['uiSettings']) => {
+ uiSettings.register({
+ [DEFAULT_SIEM_REFRESH_INTERVAL]: {
+ type: 'json',
+ name: i18n.translate('xpack.siem.uiSettings.defaultRefreshIntervalLabel', {
+ defaultMessage: 'Time filter refresh interval',
+ }),
+ value: `{
+ "pause": ${DEFAULT_INTERVAL_PAUSE},
+ "value": ${DEFAULT_INTERVAL_VALUE}
+}`,
+ description: i18n.translate('xpack.siem.uiSettings.defaultRefreshIntervalDescription', {
+ defaultMessage:
+ 'Default refresh interval for the SIEM time filter, in milliseconds.
',
+ }),
+ category: ['siem'],
+ requiresPageReload: true,
+ schema: schema.object({
+ value: schema.number(),
+ pause: schema.boolean(),
+ }),
+ },
+ [DEFAULT_SIEM_TIME_RANGE]: {
+ type: 'json',
+ name: i18n.translate('xpack.siem.uiSettings.defaultTimeRangeLabel', {
+ defaultMessage: 'Time filter period',
+ }),
+ value: `{
+ "from": "${DEFAULT_FROM}",
+ "to": "${DEFAULT_TO}"
+}`,
+ description: i18n.translate('xpack.siem.uiSettings.defaultTimeRangeDescription', {
+ defaultMessage: 'Default period of time in the SIEM time filter.
',
+ }),
+ category: ['siem'],
+ requiresPageReload: true,
+ schema: schema.object({
+ from: schema.string(),
+ to: schema.string(),
+ }),
+ },
+ [DEFAULT_INDEX_KEY]: {
+ name: i18n.translate('xpack.siem.uiSettings.defaultIndexLabel', {
+ defaultMessage: 'Elasticsearch indices',
+ }),
+ value: DEFAULT_INDEX_PATTERN,
+ description: i18n.translate('xpack.siem.uiSettings.defaultIndexDescription', {
+ defaultMessage:
+ 'Comma-delimited list of Elasticsearch indices from which the SIEM app collects events.
',
+ }),
+ category: ['siem'],
+ requiresPageReload: true,
+ schema: schema.arrayOf(schema.string()),
+ },
+ [DEFAULT_ANOMALY_SCORE]: {
+ name: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreLabel', {
+ defaultMessage: 'Anomaly threshold',
+ }),
+ value: 50,
+ type: 'number',
+ description: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreDescription', {
+ defaultMessage:
+ 'Value above which Machine Learning job anomalies are displayed in the SIEM app.
Valid values: 0 to 100.
',
+ }),
+ category: ['siem'],
+ requiresPageReload: true,
+ schema: schema.number(),
+ },
+ [ENABLE_NEWS_FEED_SETTING]: {
+ name: i18n.translate('xpack.siem.uiSettings.enableNewsFeedLabel', {
+ defaultMessage: 'News feed',
+ }),
+ value: true,
+ description: i18n.translate('xpack.siem.uiSettings.enableNewsFeedDescription', {
+ defaultMessage: 'Enables the News feed
',
+ }),
+ type: 'boolean',
+ category: ['siem'],
+ requiresPageReload: true,
+ schema: schema.boolean(),
+ },
+ [NEWS_FEED_URL_SETTING]: {
+ name: i18n.translate('xpack.siem.uiSettings.newsFeedUrl', {
+ defaultMessage: 'News feed URL',
+ }),
+ value: NEWS_FEED_URL_SETTING_DEFAULT,
+ description: i18n.translate('xpack.siem.uiSettings.newsFeedUrlDescription', {
+ defaultMessage: 'News feed content will be retrieved from this URL
',
+ }),
+ category: ['siem'],
+ requiresPageReload: true,
+ schema: schema.string(),
+ },
+ [IP_REPUTATION_LINKS_SETTING]: {
+ name: i18n.translate('xpack.siem.uiSettings.ipReputationLinks', {
+ defaultMessage: 'IP Reputation Links',
+ }),
+ value: IP_REPUTATION_LINKS_SETTING_DEFAULT,
+ type: 'json',
+ description: i18n.translate('xpack.siem.uiSettings.ipReputationLinksDescription', {
+ defaultMessage:
+ 'Array of URL templates to build the list of reputation URLs to be displayed on the IP Details page.',
+ }),
+ category: ['siem'],
+ requiresPageReload: true,
+ schema: schema.arrayOf(
+ schema.object({
+ name: schema.string(),
+ url_template: schema.string(),
+ })
+ ),
+ },
+ });
+};
diff --git a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz
index a71281c0ecfec9..3d4f0e11a7cc67 100644
Binary files a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz and b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz differ